Mocking
Al escribir pruebas, es inevitable que necesites crear una versión "falsa" de un servicio, ya sea interno o externo. Esto se conoce comúnmente como mocking (simulación). Vitest proporciona funciones de utilidad para ayudarte a través de su helper vi. Puedes import { vi } from 'vitest'
o acceder a él globalmente (cuando la configuración global está habilitada).
WARNING
¡Recuerda siempre limpiar o restaurar los mocks antes o después de cada ejecución de prueba para deshacer los cambios de estado de los mocks entre ejecuciones! Consulta la documentación de mockReset
para obtener más información.
Si quieres empezar directamente, consulta la sección de la API; si no, sigue leyendo para profundizar en el mundo del mocking.
Dates (Fechas)
A veces, necesitas tener el control de la fecha para garantizar la coherencia en las pruebas. Vitest utiliza el paquete @sinonjs/fake-timers
para manipular los temporizadores, así como la fecha del sistema. Puedes encontrar información más detallada sobre la API específica aquí.
Example (Ejemplo)
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const businessHours = [9, 17];
function purchase() {
const currentHour = new Date().getHours();
const [open, close] = businessHours;
if (currentHour > open && currentHour < close) return { message: 'Success' };
return { message: 'Error' };
}
describe('purchasing flow', () => {
beforeEach(() => {
// indica a vitest que usaremos tiempo simulado
vi.useFakeTimers();
});
afterEach(() => {
// restaurando la fecha después de cada ejecución de prueba
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// establece la hora dentro del horario comercial
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// acceder a Date.now() resultará en la fecha establecida arriba
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// establece la hora fuera del horario comercial
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// acceder a Date.now() resultará en la fecha establecida arriba
expect(purchase()).toEqual({ message: 'Error' });
});
});
Functions (Funciones)
El mocking de funciones se puede dividir en dos categorías diferentes: spying (espionaje) & mocking (simulación).
A veces, solo necesitas validar si una función específica ha sido llamada o no (y posiblemente qué argumentos se pasaron). En estos casos, un spy sería suficiente, y puedes usarlo directamente con vi.spyOn()
(lee más aquí).
Sin embargo, los spies solo pueden ayudarte a espiar funciones, no pueden modificar su implementación. En el caso de que necesitemos crear una versión falsa (o simulada) de una función, podemos usar vi.fn()
(lee más aquí).
Usamos Tinyspy como base para el mocking de funciones, pero tenemos nuestro propio wrapper para hacerlo compatible con jest
. Tanto vi.fn()
como vi.spyOn()
comparten los mismos métodos; sin embargo, solo el resultado de retorno de vi.fn()
es invocable.
Example (Ejemplo)
import { afterEach, describe, expect, it, vi } from 'vitest';
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // can also be a `getter or setter if supported`
};
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should get the latest message with a spy', () => {
const spy = vi.spyOn(messages, 'getLatest');
expect(spy.getMockName()).toEqual('getLatest');
expect(messages.getLatest()).toEqual(
messages.items[messages.items.length - 1]
);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockImplementationOnce(() => 'access-restricted');
expect(messages.getLatest()).toEqual('access-restricted');
expect(spy).toHaveBeenCalledTimes(2);
});
it('should get with a mock', () => {
const mock = vi.fn().mockImplementation(getLatest);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(1);
mock.mockImplementationOnce(() => 'access-restricted');
expect(mock()).toEqual('access-restricted');
expect(mock).toHaveBeenCalledTimes(2);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(3);
});
});
More (Más)
Globals (Variables Globales)
Puedes simular variables globales que no están presentes con jsdom
o node
utilizando el helper vi.stubGlobal
. Esto asignará el valor de la variable global a un objeto globalThis
.
import { vi } from 'vitest';
const IntersectionObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
}));
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
// ahora puedes acceder a ella como `IntersectionObserver` o `window.IntersectionObserver`
Modules (Módulos)
Los módulos simulados se utilizan para simular bibliotecas de terceros que se invocan en otro código, lo que te permite probar argumentos, la salida o incluso redeclarar su implementación.
Consulta la sección de la API vi.mock()
para obtener una descripción más detallada de la API.
Automocking algorithm (Algoritmo de Automocking)
Si tu código está importando un módulo simulado, sin ningún archivo __mocks__
asociado o factory
para este módulo, Vitest simulará el módulo en sí invocándolo y simulando cada exportación.
Se aplican los siguientes principios:
- Todos los arrays se vaciarán.
- Todas las primitivas y colecciones permanecerán igual.
- Todos los objetos se clonarán profundamente.
- Todas las instancias de clases y sus prototipos se clonarán profundamente.
Example (Ejemplo)
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// handlers (manejadores)
export function success(data) {}
export function failure(data) {}
// get todos (obtener tareas)
export async function getTodos(event, context) {
const client = new Client({
// ...clientOptions
});
await client.connect();
try {
const result = await client.query('SELECT * FROM todos;');
client.end();
return success({
message: `${result.rowCount} item(s) returned`,
data: result.rows,
status: true,
});
} catch (e) {
console.error(e.stack);
client.end();
return failure({ message: e, status: false });
}
}
vi.mock('pg', () => {
const Client = vi.fn();
Client.prototype.connect = vi.fn();
Client.prototype.query = vi.fn();
Client.prototype.end = vi.fn();
return { Client };
});
vi.mock('./handlers.js', () => {
return {
success: vi.fn(),
failure: vi.fn(),
};
});
describe('get a list of todo items', () => {
let client;
beforeEach(() => {
client = new Client();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should return items successfully', async () => {
client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(success).toBeCalledWith({
message: '0 item(s) returned',
data: [],
status: true,
});
});
it('should throw an error', async () => {
const mError = new Error('Unable to retrieve rows');
client.query.mockRejectedValueOnce(mError);
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(failure).toBeCalledWith({ message: mError, status: false });
});
});
Requests (Peticiones)
Dado que Vitest se ejecuta en Node, simular peticiones de red es complicado; las APIs web no están disponibles, por lo que necesitamos algo que simule el comportamiento de la red. Recomendamos Mock Service Worker para lograr esto. Te permitirá simular peticiones de red tanto REST
como GraphQL
, y es independiente del framework.
Mock Service Worker (MSW) funciona interceptando las peticiones que hacen tus pruebas, lo que te permite usarlo sin cambiar nada de tu código de aplicación. En el navegador, esto usa la Service Worker API. En Node.js, y para Vitest, usa node-request-interceptor. Para aprender más sobre MSW, lee su introducción
Configuration (Configuración)
Puedes usarlo como se muestra a continuación en tu archivo de configuración
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { graphql, rest } from 'msw';
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
];
export const restHandlers = [
rest.get('https://rest-endpoint.example/path/to/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(posts));
}),
];
const graphqlHandlers = [
graphql.query(
'https://graphql-endpoint.example/api/v1/posts',
(req, res, ctx) => {
return res(ctx.data(posts));
}
),
];
const server = setupServer(...restHandlers, ...graphqlHandlers);
// Inicia el servidor antes de todas las pruebas
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Cierra el servidor después de todas las pruebas
afterAll(() => server.close());
// Restablece los handlers después de cada prueba (importante para el aislamiento de las pruebas)
afterEach(() => server.resetHandlers());
Configurar el servidor con
onUnhandleRequest: 'error'
garantiza que se genere un error cuando haya una petición sin un handler correspondiente.
Example (Ejemplo)
Tenemos un ejemplo completo que usa MSW: React Testing with MSW.
More (Más)
Hay mucho más en MSW. Puedes acceder a cookies y parámetros de consulta, definir respuestas de error simuladas, ¡y mucho más! Para ver todo lo que puedes hacer con MSW, lee su documentación.
Timers (Temporizadores)
Cuando probamos código que involucra timeouts o intervals, en lugar de hacer que nuestras pruebas esperen o excedan el tiempo de espera, podemos acelerar nuestras pruebas usando temporizadores "falsos" que simulan llamadas a setTimeout
y setInterval
.
Consulta la sección de la API vi.useFakeTimers
para obtener una descripción más detallada de la API.
Example (Ejemplo)
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2); // 2 hours
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60); // 1 minute
}
const mock = vi.fn(() => console.log('executed'));
describe('delayed execution', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should execute the function', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('should not execute the function', () => {
executeAfterTwoHours(mock);
// avanzar 2ms no activará la función
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('should execute every minute', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
Hoja de Referencia
INFO
vi
en los ejemplos siguientes se importa directamente desde vitest
. También puedes usarlo globalmente si estableces globals
a true
en tu configuración.
Quiero…
- Espiar un
método
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- Simular variables exportadas
// some-path.js
export const getter = 'variable';
// some-path.test.ts
import * as exports from './some-path.js';
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked');
- Simular una función exportada
Ejemplo con vi.mock
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
Recuerda que la llamada a vi.mock
se eleva al principio del archivo. No incluyas llamadas a vi.mock
dentro de beforeEach
, ya que solo la primera simulará el módulo correctamente.
Ejemplo con vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- Simular la implementación de una clase exportada
Ejemplo con vi.mock
y prototipo:
// some-path.ts
export class SomeClass {}
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
return { SomeClass };
});
// SomeClass.mock.instances contendrá instancias de SomeClass
Ejemplo con vi.mock
y valor de retorno:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.returns contendrá el objeto retornado
Ejemplo con vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// lo que necesites de los dos primeros ejemplos
});
- Espiar un objeto devuelto por una función
Ejemplo usando caché:
// some-path.ts
export function useObject() {
return { method: () => true };
}
// useObject.js
import { useObject } from './some-path.js';
const obj = useObject();
obj.method();
// useObject.test.js
import { useObject } from './some-path.js';
vi.mock('./some-path.js', () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// ahora cada vez que se llama a useObject()
// devolverá la misma referencia al objeto
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method fue llamado dentro de some-path
expect(obj.method).toHaveBeenCalled();
- Simular parte de un módulo
import { mocked, original } from './some-path.js';
vi.mock('./some-path.js', async () => {
const mod = await vi.importActual<typeof import('./some-path.js')>(
'./some-path.js'
);
return {
...mod,
mocked: vi.fn(),
};
});
original(); // tiene el comportamiento original
mocked(); // es una función espía
- Simular la fecha actual
Para simular el tiempo de Date
, puedes usar la función auxiliar vi.setSystemTime
. Este valor no se restablecerá automáticamente entre pruebas.
Ten en cuenta que usar vi.useFakeTimers
también modifica el tiempo de Date
.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// restablecer el tiempo simulado
vi.useRealTimers();
- Simular una variable global
Puedes establecer una variable global asignando un valor a globalThis
o usando el auxiliar vi.stubGlobal
. Cuando se usa vi.stubGlobal
, no se restablecerá automáticamente entre pruebas, a menos que actives la opción de configuración unstubGlobals
o llames a vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
- Simular
import.meta.env
Para cambiar una variable de entorno, puedes simplemente asignarle un nuevo valor. Este valor no se restablecerá automáticamente entre pruebas.
import { beforeEach, expect, it } from 'vitest';
// puedes restablecerlo manualmente en el hook beforeEach
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('cambia el valor', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
Si deseas restablecer el valor automáticamente, puedes usar el auxiliar vi.stubEnv
con la opción de configuración unstubEnvs
habilitada (o llamar a vi.unstubAllEnvs
manualmente en el hook beforeEach
):
import { expect, it, vi } from 'vitest';
// antes de ejecutar las pruebas "VITE_ENV" es "test"
import.meta.env.VITE_ENV === 'test';
it('cambia el valor', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('el valor se restaura antes de ejecutar otra prueba', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};