Vitest: Cannot access 'mockLogWarn' before initialization

Declaras un mock con vi.mock, usas dentro de la fábrica una variable definida unas líneas más arriba y el archivo de pruebas ni siquiera llega a ejecutar el primer test:

ReferenceError: Cannot access 'mockLogWarn' before initialization
 ❯ createLogger src/servicio.test.ts:7:32
 ❯ src/servicio.ts:4:13

El código que lo provoca parece correcto, porque la variable está declarada antes del vi.mock:

import { describe, it, expect, vi } from 'vitest';
import { procesar } from './servicio';

const mockLogWarn = vi.fn();

vi.mock('./logger', () => ({
  createLogger: () => ({ warn: mockLogWarn }),
}));

Y el módulo bajo prueba crea su logger al importarse:

// src/servicio.ts
import { createLogger } from './logger';

// Se ejecuta al importar el módulo
const log = createLogger('servicio');

Este artículo explica por qué el orden que ves en el archivo no es el orden real de ejecución. Las comprobaciones se hicieron con Vitest 5.0.3.

Por qué pasa

La documentación de Vitest indica que vi.mock se mueve (se "hoistea") a la parte superior del archivo, de forma que se ejecuta antes de las importaciones. De ahí su advertencia: no puedes usar dentro de la fábrica variables definidas fuera de ella.

La fábrica se ejecuta cuando alguien importa por primera vez el módulo simulado. Aquí, ese alguien es servicio.ts, que llama a createLogger() en su nivel superior durante la importación, antes de que el archivo de pruebas haya ejecutado la línea const mockLogWarn = vi.fn(). Una variable const no se puede leer antes de que se ejecute su declaración, y el resultado es el ReferenceError. La traza lo muestra: la fábrica (línea 7 del test) es llamada desde la línea 4 de servicio.ts, que a su vez se importa en la línea 2 del test.

No todas las referencias fallan. Si la variable solo se lee dentro de una función que se llama más tarde, la lectura ocurre cuando el archivo ya terminó de cargarse y funciona. Por eso el error aparece cuando el código real invoca algo del módulo simulado nada más importarse: un createLogger(...) en el nivel superior, un envoltorio como export const GET = withAuth(handler) o un cliente creado al cargar el módulo.

Solución

  1. Declara la variable con vi.hoisted. Esta función ejecuta su callback antes de las importaciones estáticas y devuelve el valor que produce. Según la documentación, debe declararse antes del vi.mock que la usa:

    import { describe, it, expect, vi } from 'vitest';
    import { procesar } from './servicio';
    
    const { mockLogWarn } = vi.hoisted(() => ({ mockLogWarn: vi.fn() }));
    
    vi.mock('./logger', () => ({
      createLogger: () => ({ warn: mockLogWarn }),
    }));
    
    describe('procesar', () => {
      it('avisa con cantidad negativa', () => {
        expect(procesar(-1)).toBe(0);
        expect(mockLogWarn).toHaveBeenCalledWith('cantidad negativa');
      });
    });
  2. Si solo necesitas retrasar la lectura, envuelve la referencia en una función. Funciona únicamente cuando el código real no usa el mock durante la importación:

    vi.mock('./logger', () => ({
      createLogger: () => ({ warn: (mensaje: string) => mockLogWarn(mensaje) }),
    }));

    En la prueba, esta versión pasó porque warn solo se invoca durante el test. Si el código real llama a warn durante la importación, vuelve a fallar con el mismo error. vi.hoisted cubre ambos casos.

  3. Para valores importados, usa una importación dinámica dentro de una fábrica asíncrona. Una fábrica que lee un símbolo importado estáticamente puede fallar si el módulo que dispara la fábrica se importa antes que el que lo exporta:

    vi.mock('./logger', async () => {
      const { ApiError } = await import('./errores');
      return {
        createLogger: () => ({
          warn: () => { throw new ApiError(500, 'no debería llamarse'); },
        }),
      };
    });

    La documentación avisa de que dentro de vi.hoisted tampoco están disponibles las importaciones estáticas. Si las necesitas allí, usa await import(...) dentro del callback, aunque la propia documentación lo desaconseja salvo que no haya otra opción.

  4. Alternativa sin hoisting: vi.doMock. Funciona igual que vi.mock, pero no se mueve, así que puede usar variables locales. A cambio, solo afecta a las importaciones posteriores; importa el módulo bajo prueba después:

    const mockLogWarn = vi.fn();
    
    vi.doMock('./logger', () => ({
      createLogger: () => ({ warn: mockLogWarn }),
    }));
    
    it('avisa con cantidad negativa', async () => {
      const { procesar } = await import('./servicio');
      expect(procesar(-1)).toBe(0);
      expect(mockLogWarn).toHaveBeenCalledWith('cantidad negativa');
    });

Cómo verificar que quedó resuelto

Ejecuta solo el archivo afectado:

npx vitest run src/servicio.test.ts

La salida debe indicar que el archivo de pruebas pasó y que se ejecutaron todos sus tests, en lugar de no tests con una suite fallida. Una forma de comprobar que el mock realmente se usa es mantener la aserción expect(mockLogWarn).toHaveBeenCalledWith(...) y añadir un caso en el que no deba llamarse (por ejemplo, con una cantidad positiva), limpiando antes el mock con mockLogWarn.mockClear(). En la prueba, el archivo corregido ejecutó sus 2 tests correctamente.

Variantes y errores relacionados

El mismo error con un envoltorio de autenticación

Una ruta que exporta withAuth(handler) en su nivel superior ejecuta el envoltorio simulado al importarse. Si la fábrica lee sesionFalsa en ese momento (por ejemplo, const sesion = sesionFalsa; dentro de withAuth), el error es Cannot access 'sesionFalsa' before initialization. Si la lee dentro de la función que devuelve, que se ejecuta después, no falla. La solución definitiva es declarar sesionFalsa con vi.hoisted.

Cannot access '__vi_import_0__' before initialization

Aparece cuando una fábrica o un callback de vi.hoisted usa un símbolo importado que aún no se evaluó. En la prueba, una fábrica que leía una clase importada falló con __vi_import_1__ cuando se importaba ./servicio antes que ./errores, y funcionó con el orden contrario. Eso depende del orden de las importaciones, así que la importación dinámica de la solución 3 es más robusta.

vi importado desde otro archivo

Según la documentación, para mover vi.mock Vitest analiza estáticamente el archivo, así que vi debe importarse directamente de vitest o activarse la opción globals. Un vi reexportado desde un archivo de utilidades no se hoistea.

Solo módulos importados con import

La documentación aclara que vi.mock no funciona con require y que no simula módulos importados desde un archivo de configuración previa (setupFiles), porque ya están en caché cuando empieza el test.

Resumen

  • vi.mock se mueve al principio del archivo y su fábrica se ejecuta cuando se importa el módulo simulado, antes de que corra tu const.
  • El error aparece si el código real usa el mock durante la importación (logger, withAuth, clientes creados en el nivel superior).
  • La solución general es const { mockFn } = vi.hoisted(() => ({ mockFn: vi.fn() })), declarado antes de vi.mock.
  • Para valores importados, usa await import(...) dentro de una fábrica asíncrona, o vi.doMock con una importación posterior.

Referencias

Comentarios