Ejecutas la suite en local y todo pasa. El CI ejecuta el script con cobertura y un test falla siempre, en cada ejecución, con este mensaje:
Error: Test timed out in 5000ms.
If this is a long-running test, pass a timeout value as the last argument or
configure it globally with "testTimeout".
No es un fallo intermitente: el CI ejecuta siempre el mismo script, así que falla siempre. Este artículo explica por qué la cobertura puede empujar un test por encima del límite, cómo ajustarlo sin esconder problemas reales y qué hacer cuando el tiempo se va esperando una base de datos que no existe.
Por qué pasa
Según la documentación de Vitest, testTimeout vale 5000 ms por defecto (15 000 ms en modo navegador), y el valor 0 lo desactiva. Los hooks tienen su propio límite, hookTimeout, de 10 000 ms por defecto. Un test que supere su límite falla con el error de arriba.
Dos causas habituales:
- La cobertura hace más lento el mismo código. El proveedor por defecto,
v8, usa el perfilador de V8. La documentación de Vitest indica que puede ser más lento en proyectos que cargan muchos módulos distintos, porque V8 no puede limitar la cobertura a módulos concretos. El sobrecoste no es fijo: en una prueba sintética de laboratorio, el mismo test pasó de unos 120 ms a unos 160 ms al activar--coverage, un aumento moderado. Depende del código que se ejecute y de la máquina; un equipo con menos CPU, como un runner de CI, puede acentuarlo. Por eso el problema solo aparece si el test ya estaba cerca del límite. Se ha visto un caso real: un test que procesaba unas 3500 filas pasaba conpnpm test, pero conpnpm test:coveragetardó unos 6,6 s y superó los 5 s. - El test espera a una base de datos que no existe. Si un test usa un cliente de base de datos sin simular y la URL apunta a un servidor inalcanzable, el test espera a que el cliente se rinda. Cuánto tarda depende del controlador y del sistema: la documentación de Prisma indica, para la versión 6, un
connect_timeoutpor defecto de 5 s en la URL, y para el adaptadorpgde la versión 7, unconnectionTimeoutMillispor defecto de 0 (sin límite). En una prueba local, conectar a un puerto cerrado delocalhostfalló en milisegundos, pero un destino que descarta los paquetes puede hacer esperar mucho más; no conviene depender de que falle rápido.
Solución
-
Reproduce lo que ejecuta el CI. Compara el tiempo de cada test con y sin cobertura:
npx vitest run --reporter=verbose npx vitest run --coverage --reporter=verboseEl reporter
verbosemuestra la duración de cada test. Si el test lento queda cerca de 5 s con cobertura, ya tienes la causa. -
Pon un timeout explícito solo al test que lo necesita. Puedes pasarlo como opciones en el segundo argumento o como número en el último:
it('procesa el archivo de 3500 filas', { timeout: 30_000 }, async () => { // ... }); // equivalente it('procesa el archivo de 3500 filas', async () => { // ... }, 30_000);Ojo con una firma antigua: pasar el objeto de opciones como tercer argumento,
it('...', fn, { timeout: 30_000 }), ya no se admite en Vitest 4 y lanza unTypeErroral cargar el archivo. -
O ajústalo por suite o de forma global. Para un grupo de tests lentos:
describe('importaciones grandes', { timeout: 30_000 }, () => { // ... });Para toda la suite, en la configuración o por línea de comandos:
// vitest.config.ts export default defineConfig({ test: { testTimeout: 15_000, }, }); // o, en la línea de comandos // npx vitest run --testTimeout=15000Prefiere el timeout por test: un límite global alto puede ocultar regresiones de rendimiento en otros tests.
-
Reduce el trabajo del test. Si lo que importa es la lógica, una entrada pequeña pero representativa suele bastar. Deja la carga grande para una prueba de rendimiento aparte.
-
Simula la capa de base de datos y deja una URL inalcanzable como red de seguridad. La simulación evita la espera; la variable de entorno evita que un test olvidado llegue a una base real:
// vitest.config.ts export default defineConfig({ test: { env: { DATABASE_URL: 'postgresql://test:test@localhost:59999/mi_bd_test', }, }, }); // pedidos.test.ts import { vi } from 'vitest'; vi.mock('./db', () => ({ obtenerPedidos: vi.fn().mockResolvedValue([]), }));La opción
envde Vitest asigna variables aprocess.envantes de ejecutar los tests, yvi.mockse eleva al inicio del archivo, antes de los imports. El puerto debe ser uno donde no escuche nada en tu máquina ni en el CI.
Cómo verificar que quedó resuelto
npx vitest run
npx vitest run --coverage
Ambos comandos deben terminar sin Test timed out. Si ahora un test falla de inmediato con un error de conexión, no es un problema nuevo: significa que ese test llegaba a la base de datos sin simulación, y la red de seguridad lo ha sacado a la luz. Añade la simulación que le falta. Por último, vuelve a lanzar el pipeline del CI y comprueba que el job de cobertura queda en verde varias veces seguidas.
Variantes y errores relacionados
Timeout en hooks
Los hooks beforeAll, beforeEach, afterEach y afterAll aceptan un timeout opcional como último argumento (por defecto 10 000 ms, según la documentación de Vitest). El mensaje es similar, pero habla de Hook timed out y menciona hookTimeout.
Desactivar el límite
testTimeout: 0 lo desactiva, pero un test colgado dejará el CI esperando hasta que lo cancele el propio sistema de integración continua. No es una solución, es esconder el síntoma.
Cambiar de proveedor de cobertura
La documentación de Vitest describe istanbul como alternativa: instrumenta el código antes de ejecutarlo y puede ser más rápido en algunos casos, porque la instrumentación se puede limitar a archivos concretos. Requiere instalar @vitest/coverage-istanbul y se activa con coverage.provider: 'istanbul'. Es una opción para proyectos que cargan muchos módulos, no el primer arreglo.
Solo falla en el CI
Si el timeout aparece únicamente en el CI y también sin cobertura, revisa los recursos de la máquina (CPU, memoria) y la concurrencia de la suite antes de subir el límite.
Resumen
Test timed out in 5000mses eltestTimeoutpor defecto de Vitest; con coberturav8el mismo código tarda más, y un test cercano al límite lo supera.- Compara
vitest runconvitest run --coverage --reporter=verbosepara ver qué test se acerca a los 5 s. - Pon un timeout explícito en el test lento (
{ timeout: 30_000 }o un número como último argumento) mejor que subir el límite global. - Simula la capa de base de datos con
vi.mocky apuntaDATABASE_URLa un puerto local inalcanzable para que ningún test toque una base real. - No desactives el límite con
0: un test colgado bloquea el CI.
Comentarios
Publicar un comentario