Una prueba es una afirmación pequeña y repetible sobre lo que tu código debería hacer, verificada automáticamente en lugar de confiada a la memoria. Eso suena casi demasiado simple como para necesitar una explicación, pero casi todas las decisiones de prueba confusas en un proyecto de Node.js (cuántas pruebas de integración versus pruebas unitarias, si simular una dependencia o levantar una base de datos real, si Vitest o node:test es el ejecutor correcto) se remontan a compensaciones ocultas dentro de esa definición de una sola frase.
Conceptos básicos de pruebas pone en marcha una suite con ejemplos funcionales de node:test y Supertest; las páginas específicas de herramientas que siguen (node:test, Vitest, Testcontainers, Supertest, Contract/Pact) profundizan en una capa. Esta página se mantiene un nivel por encima: qué verifica realmente una prueba, por qué la pirámide tiene la forma que tiene y cómo las piezas (ejecutor, aserciones, dobles) se relacionan entre sí debajo de cualquier herramienta específica.
Una suite de pruebas es un portafolio de afirmaciones de comportamiento automatizadas, deliberadamente estratificadas para que las verificaciones rápidas y estrechas detecten la mayoría de los errores y las verificaciones más lentas y amplias detecten el resto.
Por qué es importante: Cada decisión de prueba es en realidad una compensación entre la confianza (cuánto de la realidad refleja una prueba) y la velocidad de retroalimentación (qué tan rápido te dice que algo anda mal); comprender esa compensación explica por qué la pirámide, no un cubo o una sola capa, es la forma que funciona.
Conceptos clave:ejecutor de pruebas, biblioteca de aserciones, doble de prueba, determinismo, fragilidad, cobertura.
Cuándo usarlo: Al decidir cuántas pruebas de integración necesita realmente una característica, al elegir entre un mock y una dependencia real, al diagnosticar una suite frágil o al explicar a un equipo por qué el 100% de cobertura no es el objetivo.
Limitaciones / Compensaciones: Ninguna suite de pruebas demuestra la corrección; demuestra que los comportamientos específicos que alguien pensó en verificar aún se mantienen. Las rutas no probadas siguen siendo tan riesgosas como si no existieran pruebas en absoluto.
Temas relacionados: pruebas unitarias vs. integración vs. contrato vs. de extremo a extremo, mocking e inyección de dependencias, aislamiento de pruebas, puertas de integración continua.
Si eliminamos las herramientas, una prueba son tres cosas que suceden en secuencia: se ejecuta algo de código (el sujeto), el resultado se compara con una expectativa (la aserción) y el resultado se informa como aprobado o fallido sin que un humano lo observe. Cada herramienta de prueba en el ecosistema de Node, desde el módulo node:test incorporado hasta Vitest y Jest, es en última instancia solo infraestructura alrededor de esa misma forma de tres pasos.
Esa infraestructura en realidad se divide en dos roles separables que son fáciles de confundir porque la mayoría de las herramientas los agrupan. Un ejecutor de pruebas descubre archivos de prueba, los ejecuta, aísla los fallos e informa los resultados; responde a la pregunta "¿qué pruebas se ejecutaron y pasaron?". Una biblioteca de aserciones proporciona las funciones de comparación dentro de una prueba (assert.equal, expect(x).toBe(y)); responde a la pregunta "¿era cierta esta afirmación específica?". El propio node:test de Node se empareja con node:assert; Vitest y Jest agrupan un ejecutor con una API de aserción de estilo expect propia. Saber que estas son preocupaciones diferentes explica por qué puedes mezclarlas y combinarlas: usar node:assert dentro de un archivo ejecutado por Vitest funciona porque al ejecutor no le importa qué estilo de aserción uses.
Una analogía útil: piensa en una suite de pruebas como una cartera de pequeñas apuestas independientes sobre el comportamiento de tu código, cada una barata de colocar y barata de verificar. Ninguna apuesta individual demuestra que todo el sistema funciona; juntas, un conjunto bien elegido de ellas hace que sea muy poco probable que algo importante esté roto en silencio.
// La forma de tres pasos, independientemente de la herramienta que la envuelvaconst result = calculateTax(100, 0.2); // el sujeto se ejecutaassert.equal(result, 20); // la aserción compara// el ejecutor informa aprobado/fallido - ningún humano lo observó suceder
La pirámide de pruebas (muchas pruebas unitarias, menos pruebas de integración, aún menos pruebas de contrato, un puñado de pruebas de extremo a extremo) no es una preferencia estilística; surge directamente de una compensación entre dos cosas que tiene cada prueba: confianza y velocidad de retroalimentación.
Una prueba unitaria que llama directamente a una función pura es rápida (milisegundos) pero estrecha: prueba que una función se comporta correctamente de forma aislada, sin decir nada sobre si está conectada correctamente a la base de datos, la capa HTTP u otro servicio. Una prueba de extremo a extremo que accede a una API implementada real prueba que todo el sistema funciona en conjunto, pero es lenta, costosa de ejecutar y toca suficientes partes móviles como para que un fallo pueda ser causado por casi cualquier cosa. La forma de pirámide (muchas pruebas rápidas y estrechas y pocas pruebas lentas y amplias) es la forma matemáticamente sensata de maximizar la confianza por segundo de tiempo de retroalimentación: detectar la mayoría de los errores de forma económica en la base y reservar las verificaciones costosas y sistémicas para las cosas que solo ellas pueden probar.
Los dobles de prueba son el mecanismo que te permite mantener una prueba estrecha a propósito, reemplazando una dependencia real con un sustituto para que la prueba aísle solo la lógica que está verificando:
Un stub devuelve una respuesta predefinida, sin lógica, útil para controlar lo que una dependencia "dice" sin preocuparse de cómo se llama.
Un mock además registra y verifica cómo se llamó, útil cuando la interacción en sí (si esta función se llamó exactamente una vez, con estos argumentos) es lo que se está probando.
Un fake es una implementación funcional pero simplificada (un repositorio en memoria que sustituye a una base de datos real), útil cuando deseas un comportamiento real sin infraestructura real.
Optar por un doble en lugar de una dependencia real es en sí mismo una compensación de confianza/velocidad en miniatura: una llamada a la base de datos con un stub se ejecuta en microsegundos, pero no puede detectar un error de sintaxis SQL real; una instancia real de Postgres a través de Testcontainers detecta ese error, pero cuesta segundos de inicio de contenedor por ejecución de suite. Ninguna opción es universalmente correcta; depende de lo que esa prueba específica esté tratando de probar.
// Un stub: respuesta predefinida, sin verificación de cómo se llamóconst repo = { findById: async () => ({ id: "1", status: "paid" }) };// Un mock: la misma forma, pero la prueba puede afirmar sobre la llamada en síconst repo = { findById: vi.fn().mockResolvedValue({ id: "1", status: "paid" }) };expect(repo.findById).toHaveBeenCalledWith("1"); // verificando la interacción
Dos propiedades determinan si todo esto es digno de confianza: determinismo y aislamiento. Una prueba determinista produce el mismo resultado en cada ejecución dado el mismo código, sin depender del tiempo del reloj, valores aleatorios o tiempos de red sin ser fijados o simulados. El aislamiento significa que la configuración de una prueba o el estado restante no pueden afectar el resultado de otra, por lo que beforeEach que restablece el estado compartido (un mapa, un mock, una transacción de base de datos) es un requisito estructural, no una preferencia de estilo. Una suite que viola cualquiera de las propiedades produce fragilidad: pruebas que fallan intermitentemente por razones no relacionadas con que el código esté mal, lo cual es corrosivo precisamente porque enseña a los ingenieros a volver a ejecutar los fallos en lugar de confiar en ellos.
La cobertura (el porcentaje de líneas o ramas que ejecuta una suite de pruebas) es uno de los números más malinterpretados en la ingeniería de software. La cobertura mide qué código se ejecutó durante la suite, no lo que realmente se verificó. Una prueba que llama a una función y no afirma nada significativo sobre su resultado sigue contando como que cubre cada línea que ejecutó esa función, sin probar nada. La cobertura es una señal útil para encontrar código que nadie prueba en absoluto; es un objetivo deficiente para optimizar directamente, porque perseguir un porcentaje recompensa las pruebas que tocan código, no las pruebas que detectan errores.
A escala, la pirámide obtiene una capa que la versión básica omite: las pruebas de contrato, que se sitúan entre la integración y el extremo a extremo. Cuando varios servicios evolucionan de forma independiente, una prueba de integración contra una dependencia real demuestra que el comportamiento actual funciona, pero no dice nada sobre si la implementación de esa dependencia mañana te romperá, y un entorno completo de extremo a extremo para cada combinación de versiones de servicio no escala. Una prueba de contrato, en cambio, captura el acuerdo entre un consumidor y un proveedor (este endpoint devuelve esta forma) y verifica ambas partes contra ese contrato compartido de forma independiente, detectando cambios importantes sin necesidad de que todos los servicios se ejecuten juntos.
Las pruebas de carga son una disciplina relacionada pero distinta que vale la pena nombrar precisamente porque es fácil agruparlas con las "pruebas" y luego aplicarles el modelo mental incorrecto. Una prueba funcional pregunta "¿es correcto el comportamiento?". Una prueba de carga pregunta "¿el comportamiento correcto se mantiene bajo concurrencia y volumen?" - está validando un objetivo de nivel de servicio (latencia, tasa de error bajo N solicitudes/segundo), no una salida específica, por lo que pertenece completamente fuera de la pirámide en lugar de ser su punta.
Capa
Fortaleza
Debilidad
Mejor ajuste
Pruebas unitarias
Retroalimentación más rápida; localiza la función rota exacta
No prueba nada sobre la conexión entre componentes
Lógica pura, cálculos, reglas de validación
Pruebas de integración
Prueba que los componentes realmente funcionan juntos (DB real, HTTP real)
Más lento; el fallo puede implicar varios componentes a la vez
Consultas de repositorio/DB, conexión de rutas HTTP
Pruebas de contrato
Detecta cambios importantes entre servicios sin un entorno completo
Tan buenas como la cobertura del contrato del uso real
Servicios implementados de forma independiente con APIs compartidas
Pruebas de extremo a extremo
Mayor confianza en que el sistema implementado realmente funciona
Más lento, más frágil, más difícil de depurar en caso de fallo
Un puñado de viajes de usuario críticos, no cobertura general
Las opciones de herramientas del ecosistema de Node se ajustan a esta misma compensación. El módulo node:test incorporado minimiza las dependencias y el costo de inicio, favoreciendo el extremo rápido de la pirámide; Vitest agrega una API de mocking más rica y ergonomía de modo de observación para equipos que realizan trabajos de unidad e integración más intensos; Testcontainers existe específicamente para hacer tolerable el costo de la "dependencia real" de la capa de integración al automatizar instancias de Docker desechables en lugar de una base de datos de prueba compartida y con estado.
"Más pruebas siempre significa más confianza." Cien pruebas unitarias sin cobertura de integración aún pueden pasar por alto un error de cableado que una sola prueba de integración bien ubicada detectaría; la confianza proviene de lo que se prueba, no del recuento.
"100% de cobertura significa que el código está bien probado." La cobertura prueba las líneas ejecutadas, no que se haya afirmado algo significativo sobre el resultado; una suite puede alcanzar el 100% y aún así pasar por alto errores reales.
"Los mocks y los stubs son lo mismo." Un stub devuelve un valor; un mock además verifica que la interacción en sí ocurrió como se esperaba. Usar un mock donde un stub sería suficiente agrega fragilidad sin ningún beneficio.
"Una prueba frágil es una molestia menor." La fragilidad daña activamente el valor de la suite; una vez que un equipo aprende a volver a ejecutar los fallos en lugar de confiar en ellos, es más probable que los fallos reales introducidos posteriormente se vuelvan a ejecutar en lugar de investigarse.
"Las pruebas de integración son estrictamente mejores que las pruebas unitarias porque son más realistas." Son realistas y lentas y más difíciles de depurar en caso de fallo; la forma de pirámide existe porque la velocidad y precisión de la capa base importan tanto como el realismo de la capa superior.
¿Qué hace que algo sea una "prueba", independientemente de la herramienta utilizada?
Tres cosas: el código se ejecuta, un resultado se compara con una expectativa y el resultado se informa automáticamente sin que un humano lo observe. Cada ejecutor y biblioteca de aserciones es infraestructura alrededor de esa misma forma.
¿Por qué la pirámide de pruebas tiene esa forma específica en lugar de ser uniforme en todas las capas?
Porque la confianza y la velocidad de retroalimentación se compensan entre sí: las pruebas estrechas son baratas y rápidas pero prueban menos, las pruebas amplias prueban más pero cuestan más. Priorizar la capa rápida y estrecha maximiza la confianza que se obtiene por segundo de tiempo de ejecución de la prueba.
¿En qué se diferencia un ejecutor de pruebas de una biblioteca de aserciones?
El ejecutor descubre, ejecuta e informa sobre los archivos de prueba; es infraestructura para ejecutar cosas. La biblioteca de aserciones proporciona las funciones de comparación utilizadas dentro de una prueba para establecer una expectativa. Algunas herramientas (Vitest, Jest) agrupan ambas; node:test y node:assert son módulos separados que se emparejan.
¿Cuál es la diferencia práctica entre un stub, un mock y un fake?
Un stub devuelve un valor predefinido sin ningún comportamiento más allá de eso.
Un mock hace lo mismo, pero también registra y puede verificar cómo se llamó.
Un fake es una implementación simplificada pero genuinamente funcional, como un almacén en memoria que sustituye a una base de datos real.
¿Cuándo debo usar una dependencia real en lugar de un doble de prueba?
Cuando lo que realmente intentas verificar depende del comportamiento real de esa dependencia; la corrección de una consulta SQL, por ejemplo, no puede ser probada por un stub que devuelve lo que le dijiste. Opta por una dependencia real (a menudo a través de Testcontainers) cuando el doble permitiría que un error real pasara desapercibido.
¿Por qué las pruebas frágiles importan tanto si eventualmente pasan?
Porque una prueba que a veces falla por razones no relacionadas con el código enseña al equipo a desconfiar y volver a ejecutar los fallos, lo que significa que es más probable que un fallo genuino introducido posteriormente se vuelva a ejecutar en lugar de investigarse.
¿Qué causa la fragilidad en primer lugar?
Generalmente una ruptura en el determinismo o el aislamiento: dependencia del tiempo real del reloj, aleatoriedad no inicializada, tiempos de red o estado residual de una prueba anterior que no se restableció entre ejecuciones.
¿Es un alto nivel de cobertura de pruebas un buen objetivo para un equipo?
No directamente; la cobertura mide qué código se ejecutó durante la suite, no lo que se verificó de manera significativa. Es útil para encontrar código completamente sin probar, pero optimizar el número en sí recompensa las pruebas que tocan código en lugar de las pruebas que detectan errores.
¿Dónde encajan las pruebas de contrato que las pruebas de integración no cubren?
Las pruebas de integración demuestran que el cableado actual contra una dependencia real funciona en este momento. Las pruebas de contrato capturan el acuerdo entre un consumidor y un proveedor explícitamente, para que cualquiera de las partes pueda verificar de forma independiente que un cambio futuro no ha roto al otro, sin necesidad de que ambos servicios se ejecuten juntos.
¿Las pruebas de carga forman parte de la pirámide de pruebas?
Realmente no; responde a una pregunta diferente. Las pruebas funcionales (la pirámide) preguntan "¿es correcto este comportamiento?". Las pruebas de carga preguntan "¿el comportamiento correcto se mantiene bajo concurrencia y volumen?", validando un objetivo de nivel de servicio en lugar de una salida específica.
¿Debo probar directamente las funciones privadas e internas?
Generalmente no; probar a través de la API pública (una función exportada, una ruta HTTP) verifica el comportamiento que realmente importa a los llamadores y sobrevive a las refactorizaciones internas que no cambian ese comportamiento. Probar los internos acopla la suite a detalles de implementación que son libres de cambiar.
¿Por qué algunos proyectos de Node ejecutan tanto `node:test` como Vitest?
Normalmente no por diseño; mezclar ejecutores en un mismo repositorio tiende a confundir a los colaboradores sobre qué suite cubre qué. La mayoría de los equipos se estandarizan en uno por repositorio, o como máximo uno por paquete en un monorepo, precisamente para evitar esa ambigüedad.