Simulación (Mocking)
Al escribir pruebas, es inevitable que necesites crear una versión "falsa" de un servicio interno o externo. Esto se conoce comúnmente como simulación o mocking. Vitest proporciona funciones de utilidad para ayudarte a través de su utilidad vi
. Puedes import { vi } from 'vitest'
o acceder a ella 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 del mock entre ejecuciones! Consulta la documentación de mockReset
para obtener más información.
Si deseas ir directamente al grano, consulta la sección de la API; de lo contrario, sigue leyendo para profundizar en el mundo de la simulación.
Fechas
A veces necesitas controlar la fecha para asegurar la consistencia al realizar pruebas. Vitest utiliza el paquete @sinonjs/fake-timers
para manipular temporizadores y la hora del sistema. Puedes encontrar más detalles sobre la API específica aquí.
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('flujo de compra', () => {
beforeEach(() => {
// Indicamos a Vitest que use tiempo simulado
vi.useFakeTimers();
});
afterEach(() => {
// Restaurando la fecha después de cada ejecución de prueba
vi.useRealTimers();
});
it('permite realizar compras durante el horario comercial', () => {
// 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 anteriormente
expect(purchase()).toEqual({ message: 'Success' });
});
it('no permite realizar compras fuera del horario comercial', () => {
// 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 anteriormente
expect(purchase()).toEqual({ message: 'Error' });
});
});
Funciones
La simulación de funciones se puede dividir en dos categorías: monitoreo y simulación.
A veces, todo lo que necesitas es validar si una función específica ha sido llamada (y posiblemente qué argumentos se pasaron). En estos casos, un "spy" sería todo lo que necesitamos, el cual puedes usar directamente con vi.spyOn()
(lee más aquí).
Sin embargo, los "spies" solo pueden ayudarte a monitorear funciones; no pueden alterar 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í).
Utilizamos Tinyspy como base para simular 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 valor de retorno de vi.fn()
es invocable.
Ejemplo
import { afterEach, describe, expect, it, vi } from 'vitest';
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // También puede ser un `getter` o `setter` (si la sintaxis lo permite)
};
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
describe('lectura de mensajes', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('debería obtener el último mensaje con un 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('debería obtener con un 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);
});
});
Más
Globales
Puedes simular variables globales que no están presentes con jsdom
o node
utilizando el asistente vi.stubGlobal
. Este asistente colocará el valor de la variable global en 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`
Módulos
Los módulos simulados permiten interceptar bibliotecas de terceros que se invocan en otro código, lo que te permite probar argumentos, salidas o incluso redeclarar su implementación.
Consulta la sección de la API vi.mock()
para una descripción más detallada de la API.
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 creará un mock del módulo invocándolo y simulando cada exportación.
Se aplican los siguientes principios:
- Todos los arrays se vaciarán.
- Todos los primitivos y colecciones permanecerán igual.
- Todos los objetos se clonarán profundamente.
- Todas las instancias de clases y sus prototipos se clonarán profundamente.
Módulos Virtuales
Vitest soporta la simulación de módulos virtuales de Vite. Difiere de cómo se tratan los módulos virtuales en Jest. En lugar de pasar virtual: true
a una función vi.mock
, debes configurar Vite para que reconozca la existencia del módulo, de lo contrario fallará durante el análisis. Puedes hacerlo de varias maneras:
- Proporcionar un alias
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js'),
},
},
};
- Proporcionar un plugin que resuelva un módulo virtual
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') {
return 'virtual:$app/forms';
}
},
},
],
};
El beneficio del segundo enfoque es que puedes crear dinámicamente diferentes entradas virtuales. Si rediriges varios módulos virtuales a un solo archivo, todos ellos serán simulados por vi.mock
, así que asegúrate de usar identificadores únicos.
Trampas del Mocking
Ten en cuenta que no es posible simular llamadas a métodos que se llaman dentro de otros métodos del mismo archivo. Por ejemplo, en este código:
export function foo() {
return 'foo';
}
export function foobar() {
return `${foo()}bar`;
}
No es posible simular el método foo
desde el exterior porque se referencia directamente. Por lo tanto, este código no tendrá ningún efecto en la llamada a foo
dentro de foobar
(pero sí afectará la llamada a foo
en otros módulos):
import { vi } from 'vitest';
import * as mod from './foobar.js';
// Esto solo tendrá efecto en "foo" cuando se acceda desde fuera del módulo original
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
return {
...(await importOriginal<typeof import('./foobar.js')>()),
// Esto solo tendrá efecto en "foo" cuando se acceda desde fuera del módulo original
foo: () => 'mocked',
};
});
Puedes confirmar este comportamiento proporcionando directamente la implementación al método foobar
:
// foobar.test.js
import * as mod from './foobar.js';
vi.spyOn(mod, 'foo');
// El foo exportado hace referencia al método simulado
mod.foobar(mod.foo);
// foobar.js
export function foo() {
return 'foo';
}
export function foobar(injectedFoo) {
return injectedFoo === foo; // false
}
Este es el comportamiento previsto. Generalmente, es un indicio de código deficiente cuando el mocking se utiliza de esta manera. Considera refactorizar tu código en múltiples archivos o mejorar la arquitectura de tu aplicación utilizando técnicas como la inyección de dependencias.
Ejemplo
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// Obtener todos los elementos
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('obtener una lista de elementos de tareas pendientes', () => {
let client;
beforeEach(() => {
client = new Client();
});
afterEach(() => {
vi.clearAllMocks();
});
it('debería devolver los elementos correctamente', 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('debería lanzar un 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 });
});
});
Sistema de Archivos
La simulación del sistema de archivos garantiza que las pruebas no dependan del sistema de archivos real, lo que las hace más fiables y predecibles. Este aislamiento ayuda a evitar efectos secundarios de pruebas anteriores. Permite probar condiciones de error y casos extremos que podrían ser difíciles o imposibles de replicar con un sistema de archivos real, como problemas de permisos, situaciones de disco lleno o errores de lectura/escritura.
Vitest no proporciona ninguna API de simulación de sistema de archivos de serie. Puedes usar vi.mock
para simular el módulo fs
manualmente, pero su mantenimiento es complicado. En su lugar, recomendamos usar memfs
para esta tarea. memfs
crea un sistema de archivos en memoria, que simula operaciones del sistema de archivos sin tocar el disco real. Este enfoque es rápido y seguro, evitando posibles efectos secundarios en el sistema de archivos real.
Ejemplo
Para redirigir automáticamente cada llamada a fs
a memfs
, puedes crear los archivos __mocks__/fs.cjs
y __mocks__/fs/promises.cjs
en la raíz de tu proyecto:
// También podemos usar `import`, pero entonces
// cada exportación debería definirse explícitamente
const { fs } = require('memfs');
module.exports = fs;
// También podemos usar `import`, pero entonces
// cada exportación debería definirse explícitamente
const { fs } = require('memfs');
module.exports = fs.promises;
// read-hello-world.js
import { readFileSync } from 'node:fs';
export function readHelloWorld(path) {
return readFileSync(path);
}
// hello-world.test.js
import { beforeEach, expect, it, vi } from 'vitest';
import { fs, vol } from 'memfs';
import { readHelloWorld } from './read-hello-world.js';
// Indica a Vitest que use el mock de fs de la carpeta __mocks__
// Esto se puede hacer en un archivo de configuración si fs siempre debe ser simulado
vi.mock('node:fs');
vi.mock('node:fs/promises');
beforeEach(() => {
// Restablece el estado del sistema de archivos en memoria
vol.reset();
});
it('debería devolver el texto correcto', () => {
const path = '/hello-world.txt';
fs.writeFileSync(path, 'hello world');
const text = readHelloWorld(path);
expect(text).toBe('hello world');
});
it('puede devolver un valor varias veces', () => {
// Puedes usar vol.fromJSON para definir varios archivos
vol.fromJSON(
{
'./dir1/hw.txt': 'hello dir1',
'./dir2/hw.txt': 'hello dir2',
},
// cwd predeterminado
'/tmp'
);
expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1');
expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2');
});
Solicitudes
Dado que Vitest se ejecuta en Node, simular solicitudes de red es complicado; las API web no están disponibles, por lo que necesitamos algo que imite el comportamiento de la red. Recomendamos Mock Service Worker para lograr esto. Te permitirá simular solicitudes de red REST
y GraphQL
, y es agnóstico al framework.
Mock Service Worker (MSW) funciona interceptando las solicitudes que realizan tus pruebas, lo que te permite usarlo sin modificar el código de tu aplicación. En navegadores, esto utiliza la API de Service Worker. En Node.js, y para Vitest, utiliza la biblioteca @mswjs/interceptors
. Para obtener más información sobre MSW, lee su introducción
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, http, HttpResponse } from 'msw';
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
];
export const restHandlers = [
http.get('https://rest-endpoint.example/path/to/posts', () => {
return HttpResponse.json(posts);
}),
];
const graphqlHandlers = [
graphql.query('ListPosts', () => {
return HttpResponse.json({
data: { posts },
});
}),
];
const server = setupServer(...restHandlers, ...graphqlHandlers);
// Iniciar el servidor antes de todas las pruebas
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Cerrar el servidor después de todas las pruebas
afterAll(() => server.close());
// Restablecer los manejadores después de cada prueba (importante para el aislamiento de pruebas)
afterEach(() => server.resetHandlers());
Configurar el servidor con
onUnhandledRequest: 'error'
asegura que se lance un error cada vez que haya una solicitud que no tenga un controlador de solicitudes correspondiente.
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.
Temporizadores
Cuando probamos código que involucra tiempos de espera o intervalos, en lugar de que nuestras pruebas esperen o se agoten, podemos acelerar nuestras pruebas utilizando temporizadores "falsos" que simulan llamadas a setTimeout
y setInterval
.
Consulta la sección de la API vi.useFakeTimers
para una descripción más detallada de la API.
Ejemplo
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2); // 2 horas
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60); // 1 minuto
}
const mock = vi.fn(() => console.log('ejecutado'));
describe('ejecución retrasada', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('debería ejecutar la función', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('no debería ejecutar la función', () => {
executeAfterTwoHours(mock);
// Avanzar 2ms no activará la función
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('debería ejecutar cada minuto', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
Clases
Puedes simular una clase completa con una sola llamada a vi.fn
; dado que todas las clases también son funciones, esto funciona de forma predeterminada. Ten en cuenta que actualmente Vitest no tiene en cuenta la palabra clave new
, por lo que new.target
siempre es undefined
en el cuerpo de una función.
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
static getType(): string {
return 'animal';
}
speak(): string {
return 'bark!';
}
isHungry() {}
feed() {}
}
Podemos recrear esta clase con funciones ES5:
const Dog = vi.fn(function (name) {
this.name = name;
});
// Observa que los métodos estáticos se mockean directamente en la función,
// no en la instancia de la clase
Dog.getType = vi.fn(() => 'mocked animal');
// Simula los métodos "speak" y "feed" en cada instancia de una clase
// Todas las instancias de `new Dog()` heredarán estos objetos spy
Dog.prototype.speak = vi.fn(() => 'loud bark!');
Dog.prototype.feed = vi.fn();
¿CUÁNDO USAR?
Por lo general, recrearías una clase como esta dentro de la fábrica de módulos si la clase se reexporta desde otro módulo:
import { Dog } from './dog.js';
vi.mock(import('./dog.js'), () => {
const Dog = vi.fn();
Dog.prototype.feed = vi.fn();
// ... otras simulaciones
return { Dog };
});
Este método también se puede usar para pasar una instancia de una clase a una función que espera la misma interfaz:
// ./src/feed.ts
function feed(dog: Dog) {
// ...
}
// ./tests/dog.test.ts
import { expect, test, vi } from 'vitest';
import { feed } from '../src/feed.js';
const Dog = vi.fn();
Dog.prototype.feed = vi.fn();
test('puede dar de comer a los perros', () => {
const dogMax = new Dog('Max');
feed(dogMax);
expect(dogMax.feed).toHaveBeenCalled();
expect(dogMax.isHungry()).toBe(false);
});
Ahora, cuando creamos una nueva instancia de la clase Dog
, su método speak
(junto con feed
) ya está simulado:
const dog = new Dog('Cooper');
dog.speak(); // ¡ladrido fuerte!
// Puedes usar aserciones incorporadas para verificar la validez de la llamada
expect(dog.speak).toHaveBeenCalled();
Podemos reasignar el valor de retorno para una instancia específica:
const dog = new Dog('Cooper');
// "vi.mocked" es una utilidad de tipo, ya que
// TypeScript no reconoce que Dog es una clase simulada,
// envuelve cualquier función en una instancia de tipo MockInstance<T>
// sin validar si la función es una simulación
vi.mocked(dog.speak).mockReturnValue('woof woof');
dog.speak(); // woof woof
Para mockear la propiedad, podemos usar el método vi.spyOn(dog, 'name', 'get')
. Esto permite usar aserciones de espía en la propiedad simulada:
const dog = new Dog('Cooper');
const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max');
expect(dog.name).toBe('Max');
expect(nameSpy).toHaveBeenCalledTimes(1);
TIP
También puedes espiar "getters" y "setters" usando el mismo método.
Hoja de trucos
INFO
vi
en los ejemplos siguientes se importa directamente de vitest
. También puedes usarlo globalmente, si configuras globals
a true
en tu configuración.
Quiero…
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
:
WARNING
No olvides que una llamada a vi.mock
se eleva a la parte superior del archivo. Siempre se ejecutará antes de todas las importaciones.
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
- 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.prototype
:
// ./some-path.ts
export class SomeClass {}
import { SomeClass } from './some-path.js';
vi.mock(import('./some-path.js'), () => {
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
return { SomeClass };
});
// SomeClass.mock.instances tendrá SomeClass
- Ejemplo con
vi.spyOn
:
import * as mod from './some-path.js';
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass);
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(import('./some-path.js'), () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// Ahora cada vez que se llame a useObject()
// devolverá la misma referencia de objeto
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method se llamó dentro de some-path
expect(obj.method).toHaveBeenCalled();
Simular parte de un módulo
import { mocked, original } from './some-path.js';
vi.mock(import('./some-path.js'), async importOriginal => {
const mod = await importOriginal();
return {
...mod,
mocked: vi.fn(),
};
});
original(); // Tiene el comportamiento original
mocked(); // Es una función mock
WARNING
No olvides que esto solo simula el acceso externo. En este ejemplo, si original
llama a mocked
internamente, siempre llamará a la función definida en el módulo, no en la fábrica de mocks.
Simular la fecha actual
Para simular la hora de Date
, puedes usar la función auxiliar vi.setSystemTime
. Este valor no se restablecerá automáticamente entre pruebas distintas.
Ten en cuenta que usar vi.useFakeTimers
también modifica la hora 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 definir una variable global asignando un valor a globalThis
o usando el asistente vi.stubGlobal
. Al usar vi.stubGlobal
, no se restablecerá automáticamente entre pruebas distintas, a menos que habilites 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, basta con asignarle un nuevo valor.
WARNING
El valor de la variable de entorno no se restablecerá automáticamente entre pruebas distintas.
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 automáticamente el/los valor/es, puedes usar el asistente
vi.stubEnv
con la opción de configuraciónunstubEnvs
habilitada (o llamar avi.unstubAllEnvs
manualmente en un hookbeforeEach
):
import { expect, it, vi } from 'vitest';
// Antes de ejecutar las pruebas "VITE_ENV" tiene el valor "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 restablece antes de la ejecución de cada prueba siguiente', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default defineConfig({
test: {
unstubEnvs: true,
},
});