Mock é essencial para testes, permitindo que você substitua dependências por implementações controladas. O Bun oferece recursos abrangentes de mock, incluindo mocks de função, spies e mocks de módulo.
Mocks de Função Básicos
Crie mocks com a função mock.
import { test, expect, mock } from "bun:test";
const random = mock(() => Math.random());
test("random", () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});Compatibilidade com Jest
Alternativamente, você pode usar a função jest.fn(), como no Jest. Ela se comporta identicamente.
import { test, expect, jest } from "bun:test";
const random = jest.fn(() => Math.random());
test("random", () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});Propriedades de Função Mock
O resultado de mock() é uma nova função que foi decorada com algumas propriedades adicionais.
import { mock } from "bun:test";
const random = mock((multiplier: number) => multiplier * Math.random());
random(2);
random(10);
random.mock.calls;
// [[ 2 ], [ 10 ]]
random.mock.results;
// [
// { type: "return", value: 0.6533907460954099 },
// { type: "return", value: 0.6452713933037312 }
// ]Propriedades e Métodos Disponíveis
As seguintes propriedades e métodos são implementados em funções mock:
| Propriedade/Método | Descrição |
|---|---|
mockFn.getMockName() | Retorna o nome do mock |
mockFn.mock.calls | Array de argumentos de chamada para cada invocação |
mockFn.mock.results | Array de valores de retorno para cada invocação |
mockFn.mock.instances | Array de contextos this para cada invocação |
mockFn.mock.contexts | Array de contextos this para cada invocação |
mockFn.mock.lastCall | Argumentos da chamada mais recente |
mockFn.mockClear() | Limpa histórico de chamadas |
mockFn.mockReset() | Limpa histórico de chamadas e remove implementação |
mockFn.mockRestore() | Restaura implementação original |
mockFn.mockImplementation(fn) | Define uma nova implementação |
mockFn.mockImplementationOnce(fn) | Define implementação apenas para próxima chamada |
mockFn.mockName(name) | Define o nome do mock |
mockFn.mockReturnThis() | Define o valor de retorno como this |
mockFn.mockReturnValue(value) | Define um valor de retorno |
mockFn.mockReturnValueOnce(value) | Define valor de retorno apenas para próxima chamada |
mockFn.mockResolvedValue(value) | Define um valor de Promise resolvida |
mockFn.mockResolvedValueOnce(value) | Define Promise resolvida apenas para próxima chamada |
mockFn.mockRejectedValue(value) | Define um valor de Promise rejeitada |
mockFn.mockRejectedValueOnce(value) | Define Promise rejeitada apenas para próxima chamada |
mockFn.withImplementation(fn, callback) | Altera temporariamente a implementação |
Exemplos Práticos
Uso Básico de Mock
import { test, expect, mock } from "bun:test";
test("comportamento de função mock", () => {
const mockFn = mock((x: number) => x * 2);
// Chamar o mock
const result1 = mockFn(5);
const result2 = mockFn(10);
// Verificar chamadas
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn).toHaveBeenLastCalledWith(10);
// Verificar resultados
expect(result1).toBe(10);
expect(result2).toBe(20);
// Inspecionar histórico de chamadas
expect(mockFn.mock.calls).toEqual([[5], [10]]);
expect(mockFn.mock.results).toEqual([
{ type: "return", value: 10 },
{ type: "return", value: 20 },
]);
});Implementações Dinâmicas de Mock
import { test, expect, mock } from "bun:test";
test("implementações dinâmicas de mock", () => {
const mockFn = mock();
// Definir implementações diferentes
mockFn.mockImplementationOnce(() => "primeiro");
mockFn.mockImplementationOnce(() => "segundo");
mockFn.mockImplementation(() => "padrão");
expect(mockFn()).toBe("primeiro");
expect(mockFn()).toBe("segundo");
expect(mockFn()).toBe("padrão");
expect(mockFn()).toBe("padrão"); // Usa implementação padrão
});Mocks Assíncronos
import { test, expect, mock } from "bun:test";
test("funções mock assíncronas", async () => {
const asyncMock = mock();
// Mock de valores resolvidos
asyncMock.mockResolvedValueOnce("primeiro resultado");
asyncMock.mockResolvedValue("resultado padrão");
expect(await asyncMock()).toBe("primeiro resultado");
expect(await asyncMock()).toBe("resultado padrão");
// Mock de valores rejeitados
const rejectMock = mock();
rejectMock.mockRejectedValue(new Error("Erro mock"));
await expect(rejectMock()).rejects.toThrow("Erro mock");
});Spies com spyOn()
É possível rastrear chamadas para uma função sem substituí-la por um mock. Use spyOn() para criar um spy; estes spies podem ser passados para .toHaveBeenCalled() e .toHaveBeenCalledTimes().
import { test, expect, spyOn } from "bun:test";
const ringo = {
name: "Ringo",
sayHi() {
console.log(`Olá, eu sou ${this.name}`);
},
};
const spy = spyOn(ringo, "sayHi");
test("spyon", () => {
expect(spy).toHaveBeenCalledTimes(0);
ringo.sayHi();
expect(spy).toHaveBeenCalledTimes(1);
});Uso Avançado de Spy
import { test, expect, spyOn, afterEach } from "bun:test";
class UserService {
async getUser(id: string) {
// Implementação original
return { id, name: `User ${id}` };
}
async saveUser(user: any) {
// Implementação original
return { ...user, saved: true };
}
}
const userService = new UserService();
afterEach(() => {
// Restaurar todos spies após cada teste
jest.restoreAllMocks();
});
test("spy em métodos de serviço", async () => {
// Spy sem alterar implementação
const getUserSpy = spyOn(userService, "getUser");
const saveUserSpy = spyOn(userService, "saveUser");
// Usar o serviço normalmente
const user = await userService.getUser("123");
await userService.saveUser(user);
// Verificar chamadas
expect(getUserSpy).toHaveBeenCalledWith("123");
expect(saveUserSpy).toHaveBeenCalledWith(user);
});
test("spy com implementação mock", async () => {
// Spy e substituir implementação
const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
id: "123",
name: "Usuário Mockado",
});
const result = await userService.getUser("123");
expect(result.name).toBe("Usuário Mockado");
expect(getUserSpy).toHaveBeenCalledWith("123");
});Mocks de Módulo com mock.module()
Mock de módulo permite substituir o comportamento de um módulo. Use mock.module(path: string, callback: () => Object) para mockar um módulo.
import { test, expect, mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});
test("mock.module", async () => {
const esm = await import("./module");
expect(esm.foo).toBe("bar");
const cjs = require("./module");
expect(cjs.foo).toBe("bar");
});Como o resto do Bun, mocks de módulo oferecem suporte tanto a import quanto require.
Substituindo Módulos Já Importados
Se você precisar substituir um módulo que já foi importado, não há nada de especial que precise fazer. Basta chamar mock.module() e o módulo será substituído.
import { test, expect, mock } from "bun:test";
// O módulo que vamos mockar está aqui:
import { foo } from "./module";
test("mock.module", async () => {
const cjs = require("./module");
expect(foo).toBe("bar");
expect(cjs.foo).toBe("bar");
// Atualizamos aqui:
mock.module("./module", () => {
return {
foo: "baz",
};
});
// E as live bindings são atualizadas.
expect(foo).toBe("baz");
// O módulo também é atualizado para CJS.
expect(cjs.foo).toBe("baz");
});Hoisting e Preloading
Se você precisar garantir que um módulo seja mockado antes de ser importado, deve usar --preload para carregar seus mocks antes de seus testes serem executados.
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});bun test --preload ./my-preloadPara facilitar sua vida, você pode colocar preload no seu bunfig.toml:
[test]
# Carregar estes módulos antes de executar testes.
preload = ["./my-preload"]Melhores Práticas de Mock de Módulo
Quando Usar Preload
O que acontece se eu mockar um módulo que já foi importado?
Se você mockar um módulo que já foi importado, o módulo será atualizado no cache de módulos. Isso significa que quaisquer módulos que importam o módulo receberão a versão mockada, MAS o módulo original ainda terá sido avaliado. Isso significa que quaisquer efeitos colaterais do módulo original ainda terão acontecido.
Se você quiser prevenir que o módulo original seja avaliado, deve usar --preload para carregar seus mocks antes de seus testes serem executados.
Exemplos Práticos de Mock de Módulo
import { test, expect, mock, beforeEach } from "bun:test";
// Mockar o módulo de cliente API
mock.module("./api-client", () => ({
fetchUser: mock(async (id: string) => ({ id, name: `User ${id}` })),
createUser: mock(async (user: any) => ({ ...user, id: "new-id" })),
updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));
test("serviço de usuário com API mockada", async () => {
const { fetchUser } = await import("./api-client");
const { UserService } = await import("./user-service");
const userService = new UserService();
const user = await userService.getUser("123");
expect(fetchUser).toHaveBeenCalledWith("123");
expect(user.name).toBe("User 123");
});Mockando Dependências Externas
import { test, expect, mock } from "bun:test";
// Mockar biblioteca de banco de dados externa
mock.module("pg", () => ({
Client: mock(function () {
return {
connect: mock(async () => {}),
query: mock(async (sql: string) => ({
rows: [{ id: 1, name: "Test User" }],
})),
end: mock(async () => {}),
};
}),
}));
test("operações de banco de dados", async () => {
const { Database } = await import("./database");
const db = new Database();
const users = await db.getUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("Test User");
});Funções Mock Globais
Limpar Todos Mocks
Redefinir todo estado de função mock (chamadas, resultados, etc.) sem restaurar sua implementação original:
import { expect, mock, test } from "bun:test";
const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());
test("limpando todos mocks", () => {
random1();
random2();
expect(random1).toHaveBeenCalledTimes(1);
expect(random2).toHaveBeenCalledTimes(1);
mock.clearAllMocks();
expect(random1).toHaveBeenCalledTimes(0);
expect(random2).toHaveBeenCalledTimes(0);
// Nota: implementações são preservadas
expect(typeof random1()).toBe("number");
expect(typeof random2()).toBe("number");
});Isso redefine as propriedades .mock.calls, .mock.instances, .mock.contexts e .mock.results de todos mocks, mas diferentemente de mock.restore(), não restaura a implementação original.
Restaurar Todos Mocks
Em vez de restaurar manualmente cada mock individualmente com mockFn.mockRestore(), restaure todos mocks com um comando chamando mock.restore(). Fazer isso não redefine o valor de módulos substituídos com mock.module().
import { expect, mock, spyOn, test } from "bun:test";
import * as fooModule from "./foo.ts";
import * as barModule from "./bar.ts";
import * as bazModule from "./baz.ts";
test("foo, bar, baz", () => {
const fooSpy = spyOn(fooModule, "foo");
const barSpy = spyOn(barModule, "bar");
const bazSpy = spyOn(bazModule, "baz");
// Valores originais
expect(fooSpy).toBe("foo");
expect(barSpy).toBe("bar");
expect(bazSpy).toBe("baz");
// Implementações mock
fooSpy.mockImplementation(() => 42);
barSpy.mockImplementation(() => 43);
bazSpy.mockImplementation(() => 44);
expect(fooSpy()).toBe(42);
expect(barSpy()).toBe(43);
expect(bazSpy()).toBe(44);
// Restaurar todos
mock.restore();
expect(fooSpy()).toBe("foo");
expect(barSpy()).toBe("bar");
expect(bazSpy()).toBe("baz");
});Usar mock.restore() pode reduzir a quantidade de código em seus testes adicionando-o a blocos afterEach em cada arquivo de teste ou até mesmo no seu código de preload de teste.
Compatibilidade com Vitest
Para maior compatibilidade com testes escritos para Vitest, o Bun fornece o objeto global vi como um alias para partes da API de mocking do Jest:
import { test, expect } from "bun:test";
// Usar o alias 'vi' similar ao Vitest
test("compatibilidade vitest", () => {
const mockFn = vi.fn(() => 42);
mockFn();
expect(mockFn).toHaveBeenCalled();
// As seguintes funções estão disponíveis no objeto vi:
// vi.fn
// vi.spyOn
// vi.mock
// vi.restoreAllMocks
// vi.clearAllMocks
});Isso facilita portar testes do Vitest para o Bun sem ter que reescrever todos seus mocks.
Detalhes de Implementação
Entender como mock.module() funciona ajuda você a usá-lo de forma mais eficaz:
Interação com Cache
Mocks de módulo interagem com caches de módulo ESM e CommonJS.
Avaliação Preguiçosa
O callback de fábrica do mock é avaliado apenas quando o módulo é realmente importado ou requerido.
Resolução de Caminho
O Bun resolve automaticamente o especificador do módulo como se estivesse fazendo um import, oferecendo suporte a:
- Caminhos relativos (
'./module') - Caminhos absolutos (
'/path/to/module') - Nomes de pacote (
'lodash')
Efeitos de Tempo de Import
- Ao mockar antes da primeira importação: Nenhum efeito colateral do módulo original ocorre
- Ao mockar após importação: Efeitos colaterais do módulo original já aconteceram
Por esta razão, usar --preload é recomendado para mocks que precisam prevenir efeitos colaterais.
Live Bindings
Módulos ESM mockados mantêm live bindings, então alterar o mock atualizará todas importações existentes.
Padrões Avançados
Funções de Fábrica
import { mock } from "bun:test";
function createMockUser(overrides = {}) {
return {
id: "mock-id",
name: "Mock User",
email: "mock@example.com",
...overrides,
};
}
const mockUserService = {
getUser: mock(async (id: string) => createMockUser({ id })),
createUser: mock(async (data: any) => createMockUser(data)),
updateUser: mock(async (id: string, data: any) => createMockUser({ id, ...data })),
};Mock Condicional
import { test, expect, mock } from "bun:test";
const shouldUseMockApi = process.env.NODE_ENV === "test";
if (shouldUseMockApi) {
mock.module("./api", () => ({
fetchData: mock(async () => ({ data: "mocked" })),
}));
}
test("uso condicional de API", async () => {
const { fetchData } = await import("./api");
const result = await fetchData();
if (shouldUseMockApi) {
expect(result.data).toBe("mocked");
}
});Padrões de Limpeza de Mock
import { afterEach, beforeEach } from "bun:test";
beforeEach(() => {
// Configurar mocks comuns
mock.module("./logger", () => ({
log: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
}));
});
afterEach(() => {
// Limpar todos mocks
mock.restore();
mock.clearAllMocks();
});Melhores Práticas
Mantenha Mocks Simples
// Bom: Mock simples e focado
const mockUserApi = {
getUser: mock(async id => ({ id, name: "Test User" })),
};
// Evite: Comportamento de mock excessivamente complexo
const complexMock = mock(input => {
if (input.type === "A") {
return processTypeA(input);
} else if (input.type === "B") {
return processTypeB(input);
}
// ... muita lógica complexa
});Use Mocks com Tipo Seguro
interface UserService {
getUser(id: string): Promise<User>;
createUser(data: CreateUserData): Promise<User>;
}
const mockUserService: UserService = {
getUser: mock(async (id: string) => ({ id, name: "Test User" })),
createUser: mock(async data => ({ id: "new-id", ...data })),
};Teste Comportamento de Mock
test("serviço chama API corretamente", async () => {
const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };
const service = new UserService(mockApi);
await service.getUser("123");
// Verificar se o mock foi chamado corretamente
expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});Notas
Auto-mocking
Diretório __mocks__ e auto-mocking ainda não são suportados. Se isso estiver bloqueando você de mudar para o Bun, por favor abra uma issue.
ESM vs CommonJS
Mocks de módulo têm implementações diferentes para módulos ESM e CommonJS. Para ES Modules, o Bun adicionou patches ao JavaScriptCore que permitem ao Bun substituir valores de exportação em tempo de execução e atualizar live bindings recursivamente.