Vitest: Test timed out in 5000ms solo con coverage

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 con pnpm test, pero con pnpm test:coverage tardó 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_timeout por defecto de 5 s en la URL, y para el adaptador pg de la versión 7, un connectionTimeoutMillis por defecto de 0 (sin límite). En una prueba local, conectar a un puerto cerrado de localhost falló 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

  1. 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=verbose

    El reporter verbose muestra la duración de cada test. Si el test lento queda cerca de 5 s con cobertura, ya tienes la causa.

  2. 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 un TypeError al cargar el archivo.

  3. 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=15000

    Prefiere el timeout por test: un límite global alto puede ocultar regresiones de rendimiento en otros tests.

  4. 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.

  5. 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 env de Vitest asigna variables a process.env antes de ejecutar los tests, y vi.mock se 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 5000ms es el testTimeout por defecto de Vitest; con cobertura v8 el mismo código tarda más, y un test cercano al límite lo supera.
  • Compara vitest run con vitest run --coverage --reporter=verbose para 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.mock y apunta DATABASE_URL a 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.

Referencias

Comentarios