Skip to content

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.

test.ts
ts
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.

test.ts
ts
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.

test.ts
ts
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étodoDescrição
mockFn.getMockName()Retorna o nome do mock
mockFn.mock.callsArray de argumentos de chamada para cada invocação
mockFn.mock.resultsArray de valores de retorno para cada invocação
mockFn.mock.instancesArray de contextos this para cada invocação
mockFn.mock.contextsArray de contextos this para cada invocação
mockFn.mock.lastCallArgumentos 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

test.ts
ts
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

test.ts
ts
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

test.ts
ts
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().

test.ts
ts
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

test.ts
ts
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.

test.ts
ts
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.

test.ts
ts
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.

my-preload.ts
ts
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
bash
bun test --preload ./my-preload

Para facilitar sua vida, você pode colocar preload no seu bunfig.toml:

bunfig.toml
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

api-client.test.ts
ts
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

database.test.ts
ts
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:

test.ts
ts
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().

test.ts
ts
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:

test.ts
ts
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

test.ts
ts
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

test.ts
ts
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

test.ts
ts
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

test.ts
ts
// 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

ts
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.ts
ts
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.

Bun by www.bunjs.com.cn edit