Una prueba unitaria puede estar en verde mientras la aplicación envía el campo equivocado a la base de datos. Una prueba de integración puede fallar porque cambió un detalle interno, aunque el comportamiento público siga siendo correcto. El problema no es elegir un lado para siempre. Es elegir el límite más pequeño que demuestre la afirmación correcta.

Usa una prueba unitaria cuando la pregunta se refiere a una unidad de comportamiento y sus colaboradores están fuera de la prueba. Usa una prueba de integración cuando la confianza depende de que varios módulos cooperen, de la serialización o de una entrada y salida real. La visión general de los tipos de pruebas de software ayuda a situar esta decisión entre las demás capas.

Los ejemplos usan JavaScript y el runner incorporado node:test. Ejecuté la fixture localmente con Node.js. Demuestra el límite de la decisión, no el comportamiento de una base de datos o un proveedor concreto.

Diagrama que compara una prueba unitaria aislada con una prueba de integración que cruza un límite real.

Respuesta corta

  • Prueba una función de forma aislada cuando la afirmación trata sobre su lógica y sus datos de entrada.
  • Cruza un límite cuando la afirmación depende de la cooperación entre módulos, la serialización o el I/O.
  • Un fake puede demostrar la colaboración dentro de tu aplicación, pero no demuestra que una base de datos o un proveedor real acepte el contrato.
  • No elijas la capa por el nombre del archivo. Elige qué debe seguir siendo real para que la prueba sea confiable.

¿Qué pregunta puede responder cada capa?

La pregunta de la prueba debe aparecer en el nombre del caso y en su preparación. Si puedes responderla con valores en memoria y una llamada directa, una prueba unitaria es un buen comienzo. Si la respuesta depende de una ruta, una consulta, bytes serializados o un servicio, la prueba debe mantener vivo ese límite.

Pregunta Capa inicial Qué sigue siendo real
¿Esta regla transforma la entrada en el resultado correcto? Unitaria La unidad bajo prueba
¿Este servicio y su repositorio coinciden en el formato? Integración estrecha Los módulos y el adaptador elegidos
¿La aplicación habla con una base de datos, archivo o API como espera? Integración externa La dependencia de prueba o un sandbox controlado
¿El usuario puede completar el flujo publicado? End-to-end La aplicación a través de su interfaz

Esta tabla no es una taxonomía rígida. Los equipos usan los nombres de formas distintas. Lo útil es declarar el límite. Martin Fowler describe la misma dificultad en "The Practical Test Pyramid": el vocabulario de los niveles de prueba no es completamente estable, pero distintas granularidades cubren riesgos diferentes.

¿Cuándo conviene una prueba unitaria?

Elige una prueba unitaria cuando el comportamiento importante cabe en una función o módulo y la prueba no necesita confirmar el comportamiento de sus colaboradores. Una regla de precios, un normalizador o una decisión de permisos puede recibir datos fijos y devolver un resultado verificable sin iniciar red, base de datos o filesystem.

Este es un módulo pequeño, sin dependencias externas:

// totals.mjs
export function totalOf(items) {
  return items.reduce(
    (total, item) => total + item.price * item.quantity,
    0,
  );
}

La prueba puede llamar a la función directamente. El colaborador real es la regla que queremos evaluar:

// totals.unit.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { totalOf } from "./totals.mjs";

test("calculates the total for each item", () => {
  assert.equal(
    totalOf([
      { price: 10, quantity: 2 },
      { price: 5, quantity: 1 },
    ]),
    25,
  );
});

El módulo node:test acepta pruebas síncronas y funciones que devuelven una Promise. La documentación de Node.js, "Test runner" también muestra que una excepción o una Promise rechazada hace fallar la prueba. Eso basta aquí porque la afirmación no depende de un puerto HTTP ni de una tabla.

Una prueba unitaria pierde valor cuando empieza a repetir la implementación. Si necesita conocer la consulta exacta, el orden de llamadas privadas y varios detalles de un adaptador, quizá esté intentando demostrar una colaboración. Prueba el comportamiento público de la unidad y deja que la integración demuestre la conversación entre las partes.

¿Cuándo hace falta una prueba de integración?

Usa una integración cuando el riesgo está en la conversación entre componentes. El código puede calcular correctamente el total y aun así guardar un objeto que el repositorio no puede aceptar. Una prueba unitaria del cálculo no puede descubrir ese acuerdo roto porque el adaptador quedó fuera de la prueba.

Una integración estrecha puede mantener reales los módulos de la aplicación y sustituir solo la infraestructura que no necesita ejercitarse en ese caso. Aquí el servicio usa un repositorio en memoria. Eso demuestra la colaboración entre servicio y repositorio, pero no afirma que PostgreSQL, los índices o las transacciones sean correctos.

// orders.mjs
import { totalOf } from "./totals.mjs";

export function createOrder({ items, orders }) {
  const order = { items, total: totalOf(items) };
  return orders.insert(order);
}
// orders.integration.test.mjs
import test from "node:test";
import assert from "node:assert/strict";
import { createOrder } from "./orders.mjs";

test("stores the total produced by the service", () => {
  const stored = [];
  const orders = {
    insert(order) {
      const saved = { id: "order-1", ...order };
      stored.push(saved);
      return saved;
    },
  };

  const result = createOrder({
    items: [{ price: 10, quantity: 2 }],
    orders,
  });

  assert.deepEqual(result, stored[0]);
  assert.equal(stored[0].total, 20);
});

El adaptador en memoria es deliberado. Mantiene el ejemplo rápido y reproducible, pero no debe llamarse integración con la base de datos. Para probar el contrato de la base de datos, añade una capa separada que inicie una base de pruebas, escriba una fila y la lea. Para una API externa, usa un sandbox o un servidor de prueba controlado. No uses producción como fixture.

¿Qué debe seguir siendo real en la prueba?

El colaborador real es una decisión sobre la afirmación, no un premio por usar menos mocks. Una prueba unitaria puede usar un stub para un puerto externo. Una prueba de integración puede usar un fake para un proveedor remoto y mantener reales el servicio, el parser y el adaptador que estás revisando.

Si el riesgo está en... Mantén real... Sustituye...
Lógica de negocio La función y sus valores Base de datos, red y reloj externo
Colaboración entre módulos El servicio y los módulos implicados Dependencias fuera del límite
JSON o protocolo HTTP Serialización, headers y parser Red externa inestable
Esquema y transacción Base de pruebas y adaptador Datos de producción

La guía de mocks de funciones de Vitest separa spies y mocks por propósito: observar una llamada no es lo mismo que proporcionar una implementación falsa. Esa diferencia ayuda a revisar una prueba, pero no convierte a Vitest en un requisito. El mismo razonamiento sirve para los fakes escritos por el equipo y las APIs de mocking de Node.js.

Si sustituyes todas las dependencias, la prueba quizá solo confirme que el código llamó a sus dobles como esperaba. Si no sustituyes ninguna, el caso puede volverse lento, frágil y difícil de diagnosticar. La mejor opción es mantener real exactamente lo que sostiene la afirmación y controlar el resto.

¿Cómo evitar demostrar lo mismo dos veces?

Una suite saludable no repite todos los casos límite en cada capa. Coloca la matriz de reglas en la capa más barata que pueda demostrarla. Después añade una integración para confirmar el límite que la unidad no cruza. Una prueba end-to-end debe añadir confianza sobre el recorrido completo, no copiar cada combinación interna.

Imagina un endpoint que calcula un pedido y guarda el total:

  1. Una prueba unitaria cubre descuento, cantidad inválida y redondeo.
  2. Una prueba de integración confirma que el servicio serializa y guarda la forma esperada.
  3. Una prueba end-to-end confirma que una petición publicada cruza autenticación, routing y respuesta.

Si la tercera prueba falla porque el total es incorrecto, la primera también debería fallar. Si solo falla la tercera porque la ruta no fue registrada, no copies la matriz de descuentos en la prueba end-to-end. Corrige la capa que no tenía una prueba.

Para un flujo de entrega de eventos, prueba el límite de un webhook separando firmas, duplicados y reintentos. Cuando la prueba depende del navegador y del recorrido publicado, usa pruebas end-to-end en el navegador para cubrir lo que las capas menores no atraviesan.

El artículo sobre probar reintentos en JavaScript sin esperar usa una separación parecida: demostrar el scheduler no es lo mismo que demostrar un efecto externo. Separar las afirmaciones hace que los fallos sean más fáciles de localizar.

¿Cómo organizar esta decisión en CI?

Separa los comandos por coste y por la infraestructura necesaria, pero no uses el nombre del comando para ocultar el límite. El runner nativo puede ejecutar todos los archivos con node --test; distintos directorios o patrones pueden organizar los casos unitarios y de integración según el proyecto.

Una convención sencilla podría ser:

{
  "scripts": {
    "test:unit": "node --test test/unit/*.test.mjs",
    "test:integration": "node --test test/integration/*.test.mjs",
    "test": "npm run test:unit && npm run test:integration"
  }
}

El ejemplo es ilustrativo. Confirma los patrones aceptados por la versión de Node.js usada en CI y asegúrate de que cada comando encuentre los archivos esperados. Si la integración necesita una base de datos, el pipeline debe crear ese servicio, esperar a que esté listo y limpiar los datos sin compartir estado entre casos.

Vitest documenta el aislamiento por archivo y permite organizar proyectos separados para pruebas unitarias y de integración. Usa esa opción si Vitest ya está en el proyecto. No introduzcas otro runner solo para poner nombres a las carpetas.

¿Qué señales muestran que la capa está equivocada?

La prueba suele estar en la capa equivocada cuando su coste y su afirmación no coinciden. Las señales aparecen en la preparación, no solo en el tiempo total de CI:

  • Una prueba llamada unitaria inicia una base de datos, red o filesystem sin que eso forme parte de la afirmación.
  • Una prueba de integración sustituye todos los módulos relevantes y nunca cruza el límite que debería proteger.
  • La prueba se rompe con cada refactor interno aunque el contrato público no haya cambiado.
  • La suite necesita sleep, un orden fijo o datos compartidos para pasar.
  • El mismo caso de negocio aparece en pruebas unitarias, de integración y end-to-end sin añadir confianza nueva.

Cuando ocurra, escribe la afirmación en una frase. Después marca los colaboradores que deben seguir siendo reales para que esa frase sea cierta. La revisión suele mostrar si necesitas una prueba unitaria rápida, una integración estrecha o una prueba externa.

Preguntas frecuentes

¿Una prueba con mock siempre es una prueba unitaria?

No. El nombre depende del límite que la prueba mantiene real. Una prueba puede combinar módulos reales y usar un fake para una API remota. Describe la afirmación y los colaboradores reales, en lugar de inferir la capa por el uso de mock.

¿Las pruebas de integración deben usar la base de datos real?

Usa la dependencia real cuando la afirmación trata sobre su contrato. Una base de pruebas aislada demuestra más que un repositorio en memoria. Aun así, debe ser desechable y estar separada de producción. Para reglas internas, la integración con la base de datos puede no ser necesaria.

¿Tengo que seguir la pirámide de pruebas?

Usa la pirámide como heurística de coste y granularidad, no como una proporción obligatoria. La mezcla depende del riesgo del sistema y del tipo de límite. La regla más estable es mantener pruebas rápidas para la lógica local y pruebas explícitas donde los datos, módulos o servicios puedan discrepar.

¿Puedo llamar integración a cualquier prueba?

Puedes usar una definición local si el equipo la mantiene de forma consistente. Registra qué incluye el nombre: dos módulos en memoria, una base de pruebas, una API sandbox o todo el sistema. La claridad del contrato importa más que ganar una discusión de vocabulario.

Conclusión

Elige una prueba unitaria cuando la afirmación cabe dentro de una unidad aislada. Elige una prueba de integración cuando la confianza depende de la colaboración, la serialización o el I/O que la unidad no cruza. Añade una prueba end-to-end solo cuando el recorrido completo aporte una prueba nueva.

El resultado no es una suite dividida por dogma. Es una suite en la que cada caso explica el riesgo que cubre, mantiene real el límite necesario y falla cerca de la causa. Cuando el siguiente cambio altere la implementación, esa separación conservará las pruebas que aún describen el comportamiento.

Fuentes consultadas