Skip to main content
Bun provides comprehensive mocking capabilities through bun:test, including function mocks, spies, and module mocks. The API is fully compatible with Jest’s mocking interface.

Creating mock functions

Use mock() to create a tracked function with a controllable implementation:
import { mock, test, expect } from "bun:test";

const fetchUser = mock(() => Promise.resolve({ id: 1, name: "Alice" }));

test("fetches user", async () => {
  const user = await fetchUser(1);
  expect(user.name).toBe("Alice");
  expect(fetchUser).toHaveBeenCalledTimes(1);
  expect(fetchUser).toHaveBeenCalledWith(1);
});

jest.fn() compatibility

jest.fn() is an alias for mock() and behaves identically:
import { test, expect, jest } from "bun:test";

const random = jest.fn(() => Math.random());

test("jest.fn works", () => {
  random();
  expect(random).toHaveBeenCalled();
});

vi compatibility

For tests migrated from Vitest, vi.fn() is also available:
import { test, expect, vi } from "bun:test";

const mockFn = vi.fn(() => 42);
mockFn();
expect(mockFn).toHaveBeenCalled();

Mock call history

Every mock records its call arguments and return values:
import { mock } from "bun:test";

const multiply = mock((x: number) => x * 2);

multiply(5);
multiply(10);

multiply.mock.calls;
// [[5], [10]]

multiply.mock.results;
// [{ type: "return", value: 10 }, { type: "return", value: 20 }]

Controlling return values

Set a fixed return value for all subsequent calls:
const getStatus = mock(() => "online");
getStatus.mockReturnValue("offline");

expect(getStatus()).toBe("offline");

Controlling implementations

mockImplementation

Replace the mock’s entire implementation:
const process = mock((x: number) => x);
process.mockImplementation(x => x * 100);

expect(process(5)).toBe(500);

mockImplementationOnce

Override the implementation for only the next call:
const fn = mock(() => "default");
fn.mockImplementationOnce(() => "first");
fn.mockImplementationOnce(() => "second");

expect(fn()).toBe("first");
expect(fn()).toBe("second");
expect(fn()).toBe("default");

Mock properties and methods reference

Property / MethodDescription
mockFn.mock.callsArray of arguments for each invocation
mockFn.mock.resultsArray of return values for each invocation
mockFn.mock.instancesArray of this contexts for each invocation
mockFn.mock.lastCallArguments of the most recent call
mockFn.mockClear()Clears call history, keeps implementation
mockFn.mockReset()Clears call history and removes implementation
mockFn.mockRestore()Restores the original implementation
mockFn.mockImplementation(fn)Sets a new implementation
mockFn.mockImplementationOnce(fn)Sets implementation for the next call only
mockFn.mockReturnValue(value)Sets a fixed return value
mockFn.mockReturnValueOnce(value)Sets return value for the next call only
mockFn.mockResolvedValue(value)Sets a resolved Promise value
mockFn.mockResolvedValueOnce(value)Sets resolved Promise for the next call only
mockFn.mockRejectedValue(value)Sets a rejected Promise value
mockFn.mockRejectedValueOnce(value)Sets rejected Promise for the next call only
mockFn.mockReturnThis()Returns this from the mock
mockFn.withImplementation(fn, callback)Temporarily replaces implementation
mockFn.getMockName()Returns the mock’s name
mockFn.mockName(name)Sets the mock’s name

Spying on existing methods

Use spyOn() to wrap an existing method with tracking without replacing it by default:
import { test, expect, spyOn } from "bun:test";

const ringo = {
  name: "Ringo",
  sayHi() {
    return `Hi, I'm ${this.name}`;
  },
};

const spy = spyOn(ringo, "sayHi");

test("tracks calls without replacing", () => {
  expect(spy).toHaveBeenCalledTimes(0);
  ringo.sayHi();
  expect(spy).toHaveBeenCalledTimes(1);
});
Spies support the same mockImplementation, mockReturnValue, etc. methods as regular mocks:
const spy = spyOn(userService, "getUser").mockResolvedValue({
  id: "1",
  name: "Mocked User",
});

const result = await userService.getUser("1");
expect(result.name).toBe("Mocked User");
expect(spy).toHaveBeenCalledWith("1");

Module mocking

Use mock.module() to replace an entire module with a controlled implementation. Both import and require are supported.
import { test, expect, mock } from "bun:test";

mock.module("./config", () => ({
  apiUrl: "http://localhost:3001",
  timeout: 1000,
}));

test("uses mocked config", async () => {
  const config = await import("./config");
  expect(config.apiUrl).toBe("http://localhost:3001");
});

Mocking external packages

mock.module("pg", () => ({
  Client: mock(function () {
    return {
      connect: mock(async () => {}),
      query: mock(async () => ({ rows: [{ id: 1, name: "Test User" }] })),
      end: mock(async () => {}),
    };
  }),
}));

Live bindings

ESM modules maintain live bindings. Updating the mock after an import is already in effect will update all existing references:
import { foo } from "./module";

test("live binding update", async () => {
  expect(foo).toBe("original");

  mock.module("./module", () => ({ foo: "mocked" }));

  // Live binding is updated
  expect(foo).toBe("mocked");
});

Preloading module mocks

To prevent the original module from executing at all, register mocks in a preload file:
my-preload.ts
import { mock } from "bun:test";

mock.module("./api-client", () => ({
  fetchUser: mock(async (id: string) => ({ id, name: "Test User" })),
}));
bunfig.toml
[test]
preload = ["./my-preload.ts"]

Global mock management

mock.restore()

Restore all mock functions to their original implementations at once. Useful in afterEach:
import { afterEach, mock } from "bun:test";

afterEach(() => {
  mock.restore();
});
mock.restore() does not reset modules overridden with mock.module().

mock.clearAllMocks()

Reset call history for all mocks without restoring implementations:
import { mock } from "bun:test";

const a = mock(() => 1);
const b = mock(() => 2);

a();
b();

mock.clearAllMocks();

expect(a).toHaveBeenCalledTimes(0);
expect(b).toHaveBeenCalledTimes(0);
// Implementations are preserved
expect(a()).toBe(1);

Best practices

Mocks with complex internal logic are hard to maintain and can introduce bugs of their own. Return the minimum data your test needs.
// Good
const mockApi = {
  getUser: mock(async (id: string) => ({ id, name: "Test User" })),
};
Always clean up to prevent test pollution:
import { afterEach, mock } from "bun:test";

afterEach(() => {
  mock.restore();
  mock.clearAllMocks();
});
interface UserService {
  getUser(id: string): Promise<User>;
}

const mockUserService: UserService = {
  getUser: mock(async (id: string) => ({ id, name: "Test User" })),
};
test("service calls API with correct arguments", async () => {
  const mockFetch = mock(async () => ({ id: "1" }));
  const service = new UserService({ fetch: mockFetch });

  await service.getUser("123");

  expect(mockFetch).toHaveBeenCalledWith("123");
  expect(mockFetch).toHaveBeenCalledTimes(1);
});

Notes

  • Auto-mocking: __mocks__ directory support is not yet implemented. File an issue if this is blocking your migration.
  • ESM live bindings: Bun patches JavaScriptCore to allow updating ESM export values at runtime, which enables live binding updates after a module is mocked.
  • Path resolution: mock.module() resolves paths the same way import does — relative paths, absolute paths, and package names are all supported.

Build docs developers (and LLMs) love