Mocking
Ao escrever testes, é comum precisar criar uma versão "falsa" de um serviço interno ou externo. Isso é chamado de mocking. O Vitest fornece funções utilitárias para auxiliar nesse processo através do helper vi. Você pode import { vi } from 'vitest'
ou acessá-lo globalmente (quando a configuração global está habilitada).
WARNING
Lembre-se sempre de limpar ou restaurar os mocks antes ou depois de cada execução de teste para reverter as alterações de estado nos mocks entre as execuções! Consulte a documentação de mockReset
para mais informações.
Se você quiser ir direto ao ponto, consulte a seção da API, caso contrário, continue lendo para se aprofundar no mundo do mocking.
Dates
Às vezes, é necessário ter controle da data para garantir a consistência ao testar. O Vitest usa o pacote @sinonjs/fake-timers
para manipular temporizadores, bem como a data do sistema. Você pode encontrar mais informações sobre a API específica em detalhes aqui.
Example
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(() => {
// informa ao Vitest que usaremos tempo simulado
vi.useFakeTimers();
});
afterEach(() => {
// restaurando a data após cada execução de teste
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// define a hora dentro do horário comercial
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// acessar Date.now() resultará na data definida acima
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// define a hora fora do horário comercial
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// acessar Date.now() resultará na data definida acima
expect(purchase()).toEqual({ message: 'Error' });
});
});
Functions
O Mocking de funções pode ser dividido em duas categorias diferentes: observação (spying) e simulação (mocking).
Às vezes, tudo o que você precisa é validar se uma função específica foi chamada ou não (e possivelmente quais argumentos foram passados). Nesses casos, um spy seria suficiente, e você pode usá-lo diretamente com vi.spyOn()
(leia mais aqui).
No entanto, os spies só podem te ajudar a espionar funções, eles não são capazes de alterar a implementação dessas funções. No caso em que precisamos criar uma versão simulada de uma função, podemos usar vi.fn()
(leia mais aqui).
Usamos Tinyspy como base para o mocking de funções, mas temos nosso próprio wrapper para torná-lo compatível com jest
. Tanto vi.fn()
quanto vi.spyOn()
compartilham os mesmos métodos, no entanto, apenas o resultado de retorno de vi.fn()
é chamável.
Example
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
Globals
Você pode simular variáveis globais que não estão presentes com jsdom
ou node
usando o auxiliar vi.stubGlobal
. Ele definirá o valor da variável global em um 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);
// agora você pode acessá-lo como `IntersectionObserver` ou `window.IntersectionObserver`
Modules
Módulos mock simulam bibliotecas de terceiros invocadas no código, permitindo testar argumentos, saídas e até mesmo redefinir suas implementações.
Veja a seção da API vi.mock()
para uma descrição da API mais detalhada e aprofundada.
Automocking algorithm
Se o seu código importar um módulo simulado, sem um arquivo __mocks__
ou factory
associado, o Vitest simulará o módulo invocando-o e simulando cada exportação.
Os seguintes princípios se aplicam:
- Todos os arrays serão esvaziados
- Todas as primitivas e coleções permanecerão as mesmas
- Todos os objetos serão clonados profundamente
- Todas as instâncias de classes e seus protótipos serão clonados profundamente
Example
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// handlers
export function success(data) {}
export function failure(data) {}
// get todos
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
Como o Vitest é executado no Node, simular requisições de rede é complicado; as APIs da web não estão disponíveis, então precisamos de algo que imite o comportamento da rede para nós. Recomendamos o Mock Service Worker para realizar isso. Ele permitirá que você simule requisições de rede REST
e GraphQL
, e é independente de framework.
O Mock Service Worker (MSW) funciona interceptando as requisições que seus testes fazem, permitindo que você o use sem alterar nenhum código do seu aplicativo. No navegador, isso usa a Service Worker API. No Node.js, e para o Vitest, ele usa node-request-interceptor. Para saber mais sobre o MSW, leia a introdução deles.
Configuration
Você pode usá-lo como abaixo no seu arquivo de configuração:
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 o servidor antes de todos os testes
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Fecha o servidor após todos os testes
afterAll(() => server.close());
// Reseta os handlers após cada teste (importante para o isolamento dos testes)
afterEach(() => server.resetHandlers());
Configurar o servidor com
onUnhandledRequest: 'error'
garante que um erro seja lançado sempre que houver uma requisição que não tenha um manipulador de requisição correspondente.
Example
Temos um exemplo completo e funcional que usa MSW: React Testing with MSW.
More
Há muito mais no MSW. Você pode acessar cookies e parâmetros de consulta, definir respostas de erro mockadas e muito mais! Para ver tudo o que você pode fazer com o MSW, leia a documentação deles.
Timers
Quando testamos código com timeouts ou intervals, em vez de esperar ou atingir o tempo limite, podemos acelerar os testes usando timers "falsos" que simulam chamadas para setTimeout
e setInterval
.
Veja a seção da API vi.useFakeTimers
para uma descrição da API mais detalhada e aprofundada.
Example
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);
// avançando em 2ms não irá disparar a função
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);
});
});
Cheat Sheet
INFO
vi
nos exemplos abaixo é importado diretamente de vitest
. Você também pode usá-lo globalmente, se definir globals
como true
em sua configuração.
Eu quero…
- Espionar um
method
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- Simular variáveis 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 função exportada
Exemplo com vi.mock
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
Não se esqueça que a chamada vi.mock
é elevada para o topo do arquivo. Não coloque chamadas vi.mock
dentro de beforeEach
, pois apenas uma delas irá realmente simular o módulo.
Exemplo com vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- Simular a implementação de uma classe exportada
Exemplo com vi.mock
e prototype:
// 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 terá SomeClass
Exemplo com vi.mock
e 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 terá o objeto retornado
Exemplo com vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// qualquer implementação que você preferir dos exemplos anteriores
});
- Espionar um objeto retornado por uma função
Exemplo usando cache:
// 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(),
};
}
// agora, toda vez que useObject() for chamado, retornará a mesma referência de objeto
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method foi chamado dentro de some-path
expect(obj.method).toHaveBeenCalled();
- Simular uma parte de um 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(); // has original behaviour
mocked(); // is a spy function
- Simular a data atual
Para simular o tempo de Date
, você pode usar a função auxiliar vi.setSystemTime
. Este valor não será automaticamente redefinido entre diferentes testes.
Esteja ciente de que usar vi.useFakeTimers
também altera o tempo de Date
.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// reset mocked time
vi.useRealTimers();
- Simular uma variável global
Você pode definir uma variável global atribuindo um valor a globalThis
ou usando o auxiliar vi.stubGlobal
. Ao usar vi.stubGlobal
, ele não será automaticamente redefinido entre diferentes testes, a menos que você habilite a opção de configuração unstubGlobals
ou chame vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
- Simular
import.meta.env
Para alterar a variável de ambiente, você pode simplesmente atribuir um novo valor a ela. Este valor não será automaticamente redefinido entre diferentes testes.
import { beforeEach, expect, it } from 'vitest';
// you can reset it in beforeEach hook manually
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('changes value', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
Se você quiser redefinir o valor automaticamente, você pode usar o auxiliar vi.stubEnv
com a opção de configuração unstubEnvs
habilitada (ou chamar vi.unstubAllEnvs
manualmente no hook beforeEach
):
import { expect, it, vi } from 'vitest';
// before running tests "VITE_ENV" is "test"
import.meta.env.VITE_ENV === 'test';
it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('the value is restored before running an other test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};