Mocking
Ao escrever testes, é comum a necessidade de criar uma versão "simulada" de um serviço interno ou externo. Isso é geralmente chamado de mocking. O Vitest oferece funções utilitárias para auxiliar nesse processo através do seu helper vi. Você pode import { vi } from 'vitest'
ou acessá-lo globalmente (quando a configuração global estiver 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 do mock entre as execuções! Consulte a documentação de mockReset
para mais informações.
Se você quiser ir direto ao ponto, confira a seção da API; caso contrário, continue lendo para explorar mais a fundo o mundo do mocking.
Datas
Às vezes, é necessário controlar a data para garantir a consistência nos testes. O Vitest utiliza o pacote @sinonjs/fake-timers
para manipular temporizadores e a data do sistema. Você pode encontrar mais detalhes sobre a API específica aqui.
Exemplo
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 estamos usando tempo mockado
vi.useFakeTimers();
});
afterEach(() => {
// restaurando a data após cada execução de teste
vi.useRealTimers();
});
it('permite compras dentro do horário comercial', () => {
// 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('não permite compras fora do horário comercial', () => {
// 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' });
});
});
Funções
O mocking de funções pode ser dividido em duas categorias: spying e mocking.
Às vezes, basta validar se uma função específica foi invocada (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 servem apenas para espionar funções; eles não conseguem alterar a implementação delas. Quando precisamos criar uma versão simulada (ou mockada) 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 o jest
. Ambos vi.fn()
e vi.spyOn()
compartilham os mesmos métodos; no entanto, apenas o valor retornado por vi.fn()
pode ser invocado.
Exemplo
import { afterEach, describe, expect, it, vi } from 'vitest';
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // pode ser um `getter` ou `setter`, se suportado
};
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
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);
});
});
Mais
Globais
Você pode simular variáveis globais que não estão disponíveis em ambientes jsdom
ou node
usando o utilitário vi.stubGlobal
. Ele atribuirá o valor da variável global ao 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`
Módulos
Módulos simulados (mock) permitem observar bibliotecas de terceiros que são invocadas em outros trechos de código, possibilitando testar argumentos, saídas ou até mesmo redefinir sua implementação.
Consulte a seção da API vi.mock()
para uma descrição mais aprofundada e detalhada da API.
Algoritmo de Automocking
Se o seu código importar um módulo simulado, sem um arquivo __mocks__
associado ou factory
para este módulo, o Vitest simulará o próprio módulo, invocando-o e simulando cada exportação.
Os seguintes princípios se aplicam:
- Todos os arrays serão zerados
- Todos os valores primitivos e coleções permanecerão inalterados
- Todos os objetos serão clonados em profundidade
- Todas as instâncias de classes e seus protótipos serão clonadas em profundidade
Módulos Virtuais
O Vitest suporta o mocking de módulos virtuais do Vite. Ele funciona de maneira distinta da forma como os módulos virtuais são tratados no Jest. Em vez de passar virtual: true
para uma função vi.mock
, você precisa indicar ao Vite a existência do módulo, caso contrário, a análise falhará. Você pode fazer isso de várias maneiras:
- Forneça um alias
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js'),
},
},
};
- Forneça um plugin que resolva um módulo virtual
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') {
return 'virtual:$app/forms';
}
},
},
],
};
A vantagem da segunda abordagem é que você pode criar dinamicamente diferentes entrypoints virtuais. Se você redirecionar vários módulos virtuais para um único arquivo, todos eles serão afetados por vi.mock
, portanto, certifique-se de usar identificadores únicos.
Armadilhas do Mocking
Atenção: não é possível simular chamadas a métodos que são invocados internamente por outros métodos no mesmo arquivo. Por exemplo, neste código:
export function foo() {
return 'foo';
}
export function foobar() {
return `${foo()}bar`;
}
Não é possível simular o método foo
externamente, pois ele é referenciado diretamente. Portanto, este código não terá efeito na invocação de foo
dentro de foobar
(mas afetará a invocação de foo
em outros módulos):
import { vi } from 'vitest';
import * as mod from './foobar.js';
// isso só terá impacto em "foo" fora do módulo original
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
return {
...(await importOriginal<typeof import('./foobar.js')>()),
// isso só terá impacto em "foo" fora do módulo original
foo: () => 'mocked',
};
});
Você pode confirmar esse comportamento fornecendo a implementação para o método foobar
diretamente:
// foobar.test.js
import * as mod from './foobar.js';
vi.spyOn(mod, 'foo');
// o `foo` exportado referencia o método simulado
mod.foobar(mod.foo);
// foobar.js
export function foo() {
return 'foo';
}
export function foobar(injectedFoo) {
return injectedFoo === foo; // false
}
Este é o comportamento esperado. Geralmente, isso é um indicativo de má prática de código quando a simulação é utilizada dessa forma. Considere refatorar seu código em múltiplos arquivos ou aprimorar a arquitetura da sua aplicação utilizando técnicas como injeção de dependência.
Exemplo
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js'; // Corrigido: Adicionado 'from' e chaves para importação
// obter tarefas
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 });
});
});
Sistema de Arquivos
Simular o sistema de arquivos garante que os testes não dependam do sistema de arquivos real, tornando-os mais confiáveis e previsíveis. Esse isolamento auxilia na prevenção de efeitos colaterais de testes anteriores. Permite testar condições de erro e casos de borda que seriam difíceis ou impossíveis de replicar com um sistema de arquivos real, como problemas de permissão, cenários de disco cheio ou erros de leitura/gravação.
O Vitest não oferece uma API de simulação de sistema de arquivos nativamente. Você pode usar vi.mock
para simular o módulo fs
manualmente, mas isso é difícil de manter. Em vez disso, recomendamos usar memfs
para essa finalidade. O memfs
cria um sistema de arquivos em memória, simulando operações de sistema de arquivos sem acessar o disco real. Essa abordagem é rápida e segura, evitando potenciais efeitos colaterais no sistema de arquivos real.
Exemplo
Para redirecionar automaticamente todas as chamadas fs
para memfs
, você pode criar os arquivos __mocks__/fs.cjs
e __mocks__/fs/promises.cjs
na raiz do seu projeto:
// também podemos usar `import`, mas, nesse caso,
// cada exportação deve ser explicitamente definida
const { fs } = require('memfs');
module.exports = fs;
// também podemos usar `import`, mas, nesse caso,
// cada exportação deve ser explicitamente definida
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';
// instrui o Vitest a usar o mock de `fs` da pasta `__mocks__`
// isso pode ser feito em um arquivo de configuração (`setup file`) se o `fs` deve ser sempre simulado
vi.mock('node:fs');
vi.mock('node:fs/promises');
beforeEach(() => {
// reseta o estado do fs em memória
vol.reset();
});
it('should return correct text', () => {
const path = '/hello-world.txt';
fs.writeFileSync(path, 'hello world');
const text = readHelloWorld(path);
expect(text).toBe('hello world');
});
it('can return a value multiple times', () => {
// você pode usar vol.fromJSON para definir vários arquivos
vol.fromJSON(
{
'./dir1/hw.txt': 'hello dir1',
'./dir2/hw.txt': 'hello dir2',
},
// cwd padrão
'/tmp'
);
expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1');
expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2');
});
Requisições
Como o Vitest é executado no Node, simular requisições de rede é desafiador; as APIs da web não estão disponíveis, então precisamos de algo que imite o comportamento da rede. Recomendamos o Mock Service Worker para conseguir isso. Ele permitirá que você simule requisições de rede REST
e GraphQL
, e é independente de frameworks.
O Mock Service Worker (MSW) funciona interceptando as requisições realizadas pelos seus testes, permitindo que você o utilize sem alterar o código da sua aplicação. No navegador, ele utiliza a API Service Worker. No Node.js, e especificamente para o Vitest, ele emprega a biblioteca @mswjs/interceptors
. Para saber mais sobre o MSW, leia a introdução deles.
Configuração
Você pode utilizá-lo conforme o exemplo abaixo em seu arquivo de configuração
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);
// Inicia o servidor antes de todos os testes
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Fecha o servidor depois de todos os testes
afterAll(() => server.close());
// Reinicia os manipuladores (*handlers*) após cada teste `importante para o isolamento dos testes`
afterEach(() => server.resetHandlers());
Configurar o servidor com
onUnhandleRequest: 'error'
garante que um erro seja lançado sempre que uma requisição não possuir um manipulador (handler) correspondente.
Mais
O MSW oferece muito mais recursos. Você pode acessar cookies e parâmetros de consulta, definir respostas de erro simuladas e muito mais! Para ver tudo o que você pode fazer com o MSW, leia a documentação deles.
Temporizadores
Ao testar código que envolve timeouts ou intervalos, em vez de esperar que nossos testes expirem, podemos acelerá-los usando temporizadores "falsos" que simulam chamadas a setTimeout
e setInterval
.
Consulte a seção da API vi.useFakeTimers
para uma descrição mais detalhada da API.
Exemplo
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('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çar por 2ms não invocará 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);
});
});
Classes
Você pode simular uma classe inteira com uma única chamada vi.fn
— como todas as classes também são funções, isso funciona de imediato. Atenção: atualmente, o Vitest não considera a palavra-chave new
, portanto, new.target
é sempre undefined
no corpo de uma função.
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
static getType(): string {
return 'animal';
}
speak(): string {
return 'bark!';
}
isHungry() {}
feed() {}
}
Podemos recriar esta classe com funções ES5:
const Dog = vi.fn(function (name) {
this.name = name;
});
// observe que os métodos estáticos são simulados diretamente na função,
// e não na instância da classe
Dog.getType = vi.fn(() => 'mocked animal');
// simula os métodos "speak" e "feed" em cada instância de uma classe
// todas as instâncias de `new Dog()` herdarão esses *spies*
Dog.prototype.speak = vi.fn(() => 'loud bark!');
Dog.prototype.feed = vi.fn();
QUANDO USAR?
Em geral, você recriaria uma classe como esta dentro da factory do módulo se a classe for reexportada de outro módulo:
import { Dog } from './dog.js';
vi.mock(import('./dog.js'), () => {
const Dog = vi.fn();
Dog.prototype.feed = vi.fn();
// ... outros mocks
return { Dog };
});
Este método também pode ser utilizado para fornecer uma instância de uma classe a uma função que aceita a mesma interface:
// ./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('can feed dogs', () => {
const dogMax = new Dog('Max');
feed(dogMax);
expect(dogMax.feed).toHaveBeenCalled();
expect(dogMax.isHungry()).toBe(false);
});
Agora, ao criar uma nova instância da classe Dog
, seu método speak
(juntamente com feed
) já estará simulado:
const dog = new Dog('Cooper');
dog.speak(); // latido alto!
// você pode usar asserções embutidas para verificar a validade da chamada
expect(dog.speak).toHaveBeenCalled();
Podemos reatribuir o valor de retorno para uma instância específica:
const dog = new Dog('Cooper');
// "vi.mocked" é um utilitário de tipagem, pois
// o TypeScript não reconhece que `Dog` é uma classe simulada,
// ele envolve qualquer função em um tipo MockInstance<T>
// sem validar se a função é uma simulação
vi.mocked(dog.speak).mockReturnValue('au au');
dog.speak(); // au au
Para simular a propriedade, podemos usar o método vi.spyOn(dog, 'name', 'get')
. Isso possibilita usar asserções de spy na propriedade 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
Você também pode espionar getters e setters usando o mesmo método.
Folha de Dicas
INFO
vi
nos exemplos abaixo é importado diretamente de vitest
. Você também pode usá-lo globalmente, caso defina globals
como true
em sua configuração.
Eu quero…
Mockar 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');
Mockar uma função exportada
- Exemplo com
vi.mock
:
WARNING
Não se esqueça que uma chamada vi.mock
é içada para o topo do arquivo. Ela sempre será executada antes de todas as importações.
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
- Exemplo com
vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
Mockar uma implementação de classe exportada
- Exemplo com
vi.mock
e.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` conterá `SomeClass`
- Exemplo com
vi.spyOn
:
import * as mod from './some-path.js';
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass);
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(import('./some-path.js'), () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// agora, cada vez que `useObject()` for chamado, ele
// retornará a mesma referência de objeto
return _cache;
};
return { useObject };
});
const obj = useObject();
// `obj.method` foi invocado dentro de `some-path`
expect(obj.method).toHaveBeenCalled();
Simular parte de um 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(); // mantém o comportamento original
mocked(); // é uma função *spy*
WARNING
Não se esqueça que isso apenas simula o acesso externo. Neste exemplo, se original
invocar mocked
internamente, ele sempre invocará a função definida no módulo, e não na mock factory.
Simular a data atual
Para simular o tempo de Date
, você pode usar a função utilitária vi.setSystemTime
. Este valor não será redefinido automaticamente entre diferentes testes.
Atenção: 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());
// redefinir o tempo simulado
vi.useRealTimers();
Simular uma variável global
Você pode definir uma variável global atribuindo um valor a globalThis
ou usando o utilitário vi.stubGlobal
. Ao usar vi.stubGlobal
, ela não será redefinida automaticamente entre diferentes testes, a menos que você habilite a opção unstubGlobals
na configuração ou chame vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
Simular import.meta.env
- Para alterar uma variável de ambiente, basta atribuir um novo valor a ela.
WARNING
O valor da variável de ambiente não será redefinido automaticamente entre diferentes testes.
import { beforeEach, expect, it } from 'vitest';
// você pode redefini-lo no *hook* beforeEach manualmente
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 automaticamente o(s) valor(es), você pode usar o utilitário
vi.stubEnv
com a opçãounstubEnvs
na configuração habilitada (ou chamarvi.unstubAllEnvs
manualmente em um hookbeforeEach
):
import { expect, it, vi } from 'vitest';
// antes da execução dos testes, "VITE_ENV" é "test"
import.meta.env.VITE_ENV === 'test';
it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('o valor é restaurado antes de executar outro teste', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default defineConfig({
test: {
unstubEnvs: true,
},
});