Inicio / Artículos / Replacing Jest with Node's Native Test Runner in Node 24

Este artículo está publicado en inglés.

Node.jsTypeScriptTestingJestVitestPerformance

Replacing Jest with Node's Native Test Runner in Node 24

A real-world migration shows how Node 24's built-in test runner and native TypeScript support cut CI time while removing four dependencies.

1565 palabras

La solicitud de integración se tituló “chore: remove jest, vitest config”, y eliminó 340 líneas junto con cuatro dependencias de desarrollo. Hubo un breve momento de preocupación por si el sistema CI se quejaría, pero no lo hizo. Cada prueba se ejecutó, todas pasaron, el informe de cobertura siguió funcionando, y todo el proceso finalizó aproximadamente un tercio más rápido que antes. Fue en ese momento cuando quedó claro que el ejecutor de pruebas integrado en Node había dejado de ser un experimento y se había convertido en una opción legítima, una que se había pasado por alto por mera costumbre durante un par de años.

Tampoco se trataba de un código base experimental. El proyecto contaba con cerca de 180 archivos de pruebas, que abarcaban casos tanto unitarios como de integración, incluyendo simulaciones realmente complejas relacionadas con temporizadores y solicitudes fetch; exactamente el tipo de conjunto de pruebas en el que recurrir a un framework con todas las funciones parece ser la opción segura por defecto.

Qué cambió realmente

El módulo node:test apareció por primera vez como una función experimental en Node 18, y por buenas razones no recibió mucha atención: las versiones iniciales carecían de un soporte sólido para la simulación, de un modo de monitoreo útil y de resultados de cobertura que parecían diseñados intencionadamente en lugar de arreglados apresuradamente. Node 24 soluciona la mayor parte de esos problemas. Ahora la información de cobertura proviene directamente de V8, sin necesidad de utilizar nyc o c8. El modo de monitoreo es lo suficientemente inteligente como para determinar qué archivos de pruebas se ven afectados por un cambio concreto, en lugar de ejecutar nuevamente toda la suite de pruebas de forma indiscriminada. Además, la simulación de temporizadores, módulos y funciones está integrada, por lo que no hay necesidad de incluir dependencias adicionales solo para simular un reloj o crear una versión ficticia de una función.

import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { fetchUser } from './users.js';
test('fetchUser devuelve datos normalizados', async (t) => {
 const fetchMock = mock.method(global, 'fetch', async () =>
 new Response(JSON.stringify({ id: 1, name: 'test' }))
 ); const user = await fetchUser(1); assert.equal(user.id, 1);
 assert.equal(fetchMock.mock.callCount(), 1);
});

Se ejecuta con node --test — sin archivo de configuración que mantener, sin paso de Babel, sin capa de transformación ts-jest que añada silenciosamente unos segundos a cada archivo. Este último punto resultó ser más importante que el propio ejecutor de pruebas.

La parte nativa de TypeScript

Node ahora puede ejecutar archivos .ts directamente al eliminar los tipos en el momento del análisis, en lugar de compilarlos de la forma tradicional; a partir de Node 24, esta funcionalidad pasó de ser una opción experimental a la configuración predeterminada para la mayoría de las sintaxis. Ya no es necesario utilizar ts-node, ni tsx, ni realizar un paso de compilación adicional solo para ejecutar scripts o pruebas.

node app.ts
node --test tests/

Hay un problema real aquí: el proceso de eliminación de tipos no realiza ninguna verificación de tipo. Simplemente elimina las anotaciones y ejecuta el código que queda. Si su base de código depende de características como enums que contienen valores en tiempo de ejecución, nombres de espacio o abreviaturas para parámetros de constructores, algunas de estas necesitan flags adicionales o aún no están soportadas, ya que en realidad generan salida en JavaScript en lugar de poder ser eliminadas por completo. Esto no está diseñado para reemplazar al compilador de TypeScript, ni lo intenta. Todavía necesita tsc --noEmit o herramientas de editor para detectar errores reales de tipo. Lo que sí elimina es la carga constante de compilar un archivo solo para ejecutarlo, un costo que siempre ha formado parte del flujo de trabajo de TypeScript.

Por qué la suite de pruebas se volvió más rápida

Deshacerse de Jest no solo eliminó una dependencia, sino que también suprimió todo un pipeline de transformación. Por defecto, Jest maneja TypeScript de una de dos maneras: o bien lo pasa a ts-jest, que es exhaustivo pero lento porque realiza una verificación completa de tipos en cada archivo a menos que se desactive explícitamente, o bien utiliza babel-jest, que es más rápido pero añade su propio nivel de configuración con sus propios casos límite relacionados con decoradores y sintaxis más reciente. Cuando Node ejecuta archivos .ts de forma nativa, el propio ejecutor omite por completo ese paso de transformación. Añada a esto el hecho de que node:test es, según se dice, un 40% más rápido que las versiones anteriores del ejecutor de pruebas de Node en cargas de trabajo similares, y una reducción de un tercio en el tiempo total del conjunto de pruebas deja de parecer una coincidencia; en realidad se trata de dos mejoras de rendimiento independientes que se suman entre sí.

Se esperaba que la generación de informes de cobertura generara conflictos, pero no fue así.

node --test --experimental-test-coverage tests/

El formato no es tan refinado como el que Istanbul produce por defecto en HTML, pero los porcentajes de cobertura subyacentes coincidieron con las cifras de c8 hasta dentro de un punto, y para controlar las solicitudes de pull request en CI, ese nivel de precisión es realmente lo único que importa.

Dónde sigue fallando

La prueba por captura de estado es la verdadera limitación aquí. Los equipos que dependen en gran medida de la función de captura de estado de Jest, ya sea para el renderizado de componentes o para registrar la estructura de las respuestas de API, aún no encuentran un reemplazo integrado. Tienen que escribir su propia lógica de comparación o utilizar una biblioteca separada de captura de estado solo para abordar ese caso de uso. Lo mismo ocurre con cualquier cosa que se construya a partir del mock automático de módulos de Jest mediante jest.mock('./path') y su comportamiento de elevación. mock.method y mock.module de Node manejan gran parte de esto, pero usarlos implica ser más explícitos y detallados respecto a qué se sustituye y en qué momento.

En un proyecto centrado en componentes React con extensas pruebas de interfaz basadas en snapshots, espere que esta transición sea considerablemente más difícil que la que se experimentó con el servicio backend utilizado en esta evaluación. El cambio funciona casi sin problemas actualmente en el código del lado servidor y en las herramientas CLI.

La ejecución paralela es otro factor que vale la pena verificar antes de adoptar este enfoque en un conjunto de pruebas de tamaño considerable. La arquitectura de grupo de trabajadores de Jest no distribuye los archivos de prueba entre procesos de la misma manera que lo hace el ejecutor integrado de Node, y dependiendo de la estructura de su conjunto de pruebas, el tiempo total podría variar de manera indeseada en un monorepo grande, aunque los archivos de prueba individuales se ejecuten más rápido de forma aislada. Vale la pena medir esto en las máquinas reales que utiliza su pipeline de CI, en lugar de en una laptop local con varios núcleos ociosos y sin competencia por recursos.

Así fue en realidad la migración

Para quienes estén considerando este cambio, aquí está un resumen aproximado de cómo se desarrolló: ambos ejecutores de pruebas funcionaron en paralelo dentro del CI durante aproximadamente una semana, en lugar de hacer el cambio de golpe en una sola solicitud de pull. Los mismos archivos de pruebas se procesaron a través de dos tareas separadas en el CI, y sus resultados y tiempos se compararon directamente. Ese proceso reveló dos pruebas que dependían silenciosamente de variables globales específicas de Jest, cuya existencia nadie recordaba haber introducido; ambas se corrigieron en menos de una hora una vez identificadas. Solo cuando las dos tareas del CI arrojaron resultados consistentes durante toda una semana se presentó realmente la solicitud de pull para eliminar Jest. Es un método lento y poco llamativo, pero cuando se manipula la red de seguridad destinada a detectar errores en otros lugares, un proceso aburrido pero reversible es mejor que uno rápido pero imposible de deshacer.

¿Y qué pasa con Vitest?

Esto merece abordarse directamente, ya que es la alternativa que la mayoría de las personas menciona primero. Vitest sí funciona más rápido que Jest, ofrece una API más agradable y se integra perfectamente en los flujos de trabajo front-end basados en Vite; nada de eso está en discusión. Sin embargo, sigue siendo una dependencia adicional con su propia configuración, capaz de desviarse de la versión de Node que realmente se esté utilizando y causar errores que pueden consumir toda una tarde. Si un código front-end ya está construido alrededor de Vite, seguir siendo razonable elegir Vitest, ya que aún no existe nada nativo que reemplace las pruebas de componentes al estilo jsdom. En cambio, para un servicio back-end o una herramienta de línea de comandos sin bundler, incorporar Vitest únicamente por una sintaxis de afirmaciones más agradable perdió su atractivo una vez que node:test cerró la brecha en cuanto a simulaciones y cobertura. Es una herramienta adecuada para un tipo específico de proyecto, no un reemplazo total para cualquier caso.

Configuración de ry.

Qué implica esto

No se trata de un llamado a reescribir inmediatamente toda la base de código existente. Sin embargo, en el futuro ya no existe una razón clara y predeterminada para utilizar un ejecutor de pruebas externo al iniciar un nuevo servicio Node, algo que no habría sido válido hace un año. El ecosistema dedicó casi diez años a crear herramientas complejas para cubrir las deficiencias que el propio entorno de ejecución dejaba abiertas, y ahora que algunas de esas deficiencias se han cerrado, una parte de esas herramientas representa un peso innecesario. No toda la cadena de herramientas, para ser claros; solo una proporción mayor de lo esperado.

Lecturas relacionadas

  • Comparación de los agentes de IA Frontier: Astra, Flash, Fable y Mythos — Un análisis de cómo se desempeñan las últimas versiones de los modelos GPT, Gemini y Claude en tareas reales de agencia como programación, navegación y uso de herramientas, y no solo en pruebas de rendimiento.
  • Ajuste de los nuevos controles de chunking de Turbopack en Next.js 16.3 — Una explicación práctica de la nueva configuración turbopackChunking en Next.js 16.3, donde se detalla cómo maxChunkCountPerGroup y generateComponentChunks influyen en el tamaño del paquete y en el caché.
  • La reescritura en Go de TypeScript 7: qué implica para la seguridad de tipos en React — Aprenda cómo el compilador basado en Go de TypeScript 7 acelera los procesos de compilación y mejora la inferencia genérica, eliminando los tipos any ocultos en los hooks y JSX de React.
  • Dentro de los interceptores de NestJS: cómo solucionar una regresión de latencia del 96% a gran escala — Aprenda cómo el pipeline de ejecución AOP de NestJS y los problemas al desmontar componentes con RxJS causaron un aumento en la latencia P99, y cómo crear un interceptor de auditoría sin asignaciones para solucionarlo.