Skip to main content
Mocking replaces a dependency with a controlled implementation. Bun supports function mocks, spies, and module mocks.

Basic Function Mocks

Create mocks with the mock function.
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.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);
});

Jest Compatibility

You can also use jest.fn(), as in Jest. It behaves identically.
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.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);
});

Mock Function Properties

mock() returns a new function decorated with additional properties.
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.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 }
//  ]

Available Properties and Methods

Mock functions implement the following properties and methods:
Property/MethodDescription
mockFn.getMockName()Returns the mock name
mockFn.mock.callsArray of call arguments for each invocation
mockFn.mock.resultsArray of return values for each invocation
mockFn.mock.instancesArray of instances created with new
mockFn.mock.contextsArray of this contexts for each invocation
mockFn.mock.lastCallArguments of the most recent call
mockFn.mockClear()Clears call history
mockFn.mockReset()Clears call history and removes implementation
mockFn.mockRestore()Restores original implementation
mockFn.mockImplementation(fn)Sets a new implementation
mockFn.mockImplementationOnce(fn)Sets implementation for next call only
mockFn.mockName(name)Sets the mock name
mockFn.mockReturnThis()Sets the return value to this
mockFn.mockReturnValue(value)Sets a return value
mockFn.mockReturnValueOnce(value)Sets return value for next call only
mockFn.mockResolvedValue(value)Sets a resolved Promise value
mockFn.mockResolvedValueOnce(value)Sets resolved Promise for next call only
mockFn.mockRejectedValue(value)Sets a rejected Promise value
mockFn.mockRejectedValueOnce(value)Sets rejected Promise for next call only
mockFn.withImplementation(fn, callback)Temporarily changes implementation

Practical Examples

Basic Mock Usage

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { test, expect, mock } from "bun:test";

test("mock function behavior", () => {
  const mockFn = mock((x: number) => x * 2);

  // Call the mock
  const result1 = mockFn(5);
  const result2 = mockFn(10);

  // Verify calls
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith(5);
  expect(mockFn).toHaveBeenLastCalledWith(10);

  // Check results
  expect(result1).toBe(10);
  expect(result2).toBe(20);

  // Inspect call history
  expect(mockFn.mock.calls).toEqual([[5], [10]]);
  expect(mockFn.mock.results).toEqual([
    { type: "return", value: 10 },
    { type: "return", value: 20 },
  ]);
});

Dynamic Mock Implementations

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { test, expect, mock } from "bun:test";

test("dynamic mock implementations", () => {
  const mockFn = mock();

  // Set different implementations
  mockFn.mockImplementationOnce(() => "first");
  mockFn.mockImplementationOnce(() => "second");
  mockFn.mockImplementation(() => "default");

  expect(mockFn()).toBe("first");
  expect(mockFn()).toBe("second");
  expect(mockFn()).toBe("default");
  expect(mockFn()).toBe("default"); // Uses default implementation
});

Async Mocks

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { test, expect, mock } from "bun:test";

test("async mock functions", async () => {
  const asyncMock = mock();

  // Mock resolved values
  asyncMock.mockResolvedValueOnce("first result");
  asyncMock.mockResolvedValue("default result");

  expect(await asyncMock()).toBe("first result");
  expect(await asyncMock()).toBe("default result");

  // Mock rejected values
  const rejectMock = mock();
  rejectMock.mockRejectedValue(new Error("Mock error"));

  await expect(rejectMock()).rejects.toThrow("Mock error");
});

Spies with spyOn()

Use spyOn() to track calls to a function without replacing it with a mock. Spies can be passed to .toHaveBeenCalled() and .toHaveBeenCalledTimes().
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { test, expect, spyOn } from "bun:test";

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

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

test("spyon", () => {
  expect(spy).toHaveBeenCalledTimes(0);
  ringo.sayHi();
  expect(spy).toHaveBeenCalledTimes(1);
});

Advanced Spy Usage

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { test, expect, spyOn, afterEach } from "bun:test";

class UserService {
  async getUser(id: string) {
    // Original implementation
    return { id, name: `User ${id}` };
  }

  async saveUser(user: any) {
    // Original implementation
    return { ...user, saved: true };
  }
}

const userService = new UserService();

afterEach(() => {
  // Restore all spies after each test
  jest.restoreAllMocks();
});

test("spy on service methods", async () => {
  // Spy without changing implementation
  const getUserSpy = spyOn(userService, "getUser");
  const saveUserSpy = spyOn(userService, "saveUser");

  // Use the service normally
  const user = await userService.getUser("123");
  await userService.saveUser(user);

  // Verify calls
  expect(getUserSpy).toHaveBeenCalledWith("123");
  expect(saveUserSpy).toHaveBeenCalledWith(user);
});

test("spy with mock implementation", async () => {
  // Spy and override implementation
  const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
    id: "123",
    name: "Mocked User",
  });

  const result = await userService.getUser("123");

  expect(result.name).toBe("Mocked User");
  expect(getUserSpy).toHaveBeenCalledWith("123");
});

Module Mocks with mock.module()

Use mock.module(path: string, callback: () => Object) to override the behavior of a module.
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.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");
});
Like the rest of Bun, module mocks support both import and require.

Overriding Already Imported Modules

Calling mock.module() overrides the module even if it has already been imported.
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { test, expect, mock } from "bun:test";

// The module we're going to mock is here:
import { foo } from "./module";

test("mock.module", async () => {
  const cjs = require("./module");
  expect(foo).toBe("bar");
  expect(cjs.foo).toBe("bar");

  // We update it here:
  mock.module("./module", () => {
    return {
      foo: "baz",
    };
  });

  // And the live bindings are updated.
  expect(foo).toBe("baz");

  // The module is also updated for CJS.
  expect(cjs.foo).toBe("baz");
});

Hoisting & Preloading

To make sure a module is mocked before it’s imported, use --preload to load your mocks before your tests run.
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bmy-preload.ts
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
terminal
bun test --preload ./my-preload
To avoid typing --preload every time you run tests, add it to your bunfig.toml:
bunfig.toml
[test]
# Load these modules before running tests.
preload = ["./my-preload"]

Module Mock Best Practices

When to Use Preload

Mocking a module that’s already been imported updates the module cache, so anything that imports it gets the mocked version. The original module has already been evaluated, though, so its side effects have already happened. To prevent the original module from being evaluated at all, use --preload to load your mocks before your tests run.

Practical Module Mock Examples

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bapi-client.test.ts
import { test, expect, mock, beforeEach } from "bun:test";

// Mock the API client module
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("user service with mocked API", 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");
});

Mocking External Dependencies

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35bdatabase.test.ts
import { test, expect, mock } from "bun:test";

// Mock external database library
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("database operations", 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");
});

Global Mock Functions

Clear All Mocks

mock.clearAllMocks() resets the .mock.calls, .mock.instances, .mock.contexts, and .mock.results properties of every mock. Unlike mock.restore(), it does not restore the original implementation:
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { expect, mock, test } from "bun:test";

const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());

test("clearing all mocks", () => {
  random1();
  random2();

  expect(random1).toHaveBeenCalledTimes(1);
  expect(random2).toHaveBeenCalledTimes(1);

  mock.clearAllMocks();

  expect(random1).toHaveBeenCalledTimes(0);
  expect(random2).toHaveBeenCalledTimes(0);

  // Note: implementations are preserved
  expect(typeof random1()).toBe("number");
  expect(typeof random2()).toBe("number");
});

Reset All Mocks

jest.resetAllMocks() (and its vi.resetAllMocks() alias) calls mockFn.mockReset() on every mock: on top of what clearAllMocks() does, it drops the implementations set by mockImplementation(), mockReturnValue() and friends. It does not restore the original implementation of a spy:
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { expect, jest, test } from "bun:test";

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

test("resetting all mocks", () => {
  random();
  expect(random).toHaveBeenCalledTimes(1);

  jest.resetAllMocks();

  expect(random).toHaveBeenCalledTimes(0);
  // unlike clearAllMocks(), the implementation is gone
  expect(random()).toBeUndefined();
});

Restore All Mocks

mock.restore() restores every mock at once, instead of calling mockFn.mockRestore() on each one. It does not reset modules overridden with mock.module().
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.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");

  // Original implementations still work
  expect(fooModule.foo()).toBe("foo");
  expect(barModule.bar()).toBe("bar");
  expect(bazModule.baz()).toBe("baz");

  // Mock implementations
  fooSpy.mockImplementation(() => 42);
  barSpy.mockImplementation(() => 43);
  bazSpy.mockImplementation(() => 44);

  expect(fooModule.foo()).toBe(42);
  expect(barModule.bar()).toBe(43);
  expect(bazModule.baz()).toBe(44);

  // Restore all
  mock.restore();

  expect(fooModule.foo()).toBe("foo");
  expect(barModule.bar()).toBe("bar");
  expect(bazModule.baz()).toBe("baz");
});
Call mock.restore() in an afterEach block, or in your test preload script, instead of repeating cleanup in every test.

Vitest Compatibility

For added compatibility with tests written for Vitest, Bun provides the vi object as an alias for parts of the Jest mocking API:
https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { test, expect, vi } from "bun:test";

// Using the 'vi' alias similar to Vitest
test("vitest compatibility", () => {
  const mockFn = vi.fn(() => 42);

  mockFn();
  expect(mockFn).toHaveBeenCalled();

  // The following functions are available on the vi object:
  // vi.fn
  // vi.spyOn
  // vi.mock
  // vi.restoreAllMocks
  // vi.resetAllMocks
  // vi.clearAllMocks
});
You can port tests from Vitest without rewriting your mocks.

Implementation Details

Cache Interaction

Module mocks interact with both ESM and CommonJS module caches.

Lazy Evaluation

The mock factory callback is only evaluated when the module is imported or required.

Path Resolution

Bun resolves the module specifier the same way it resolves an import, supporting:
  • Relative paths ('./module')
  • Absolute paths ('/path/to/module')
  • Package names ('lodash')

Import Timing Effects

  • When mocking before first import: No side effects from the original module occur
  • When mocking after import: The original module’s side effects have already happened
For this reason, use --preload for mocks that need to prevent side effects.

Live Bindings

Mocked ESM modules maintain live bindings, so changing the mock updates all existing imports.

Advanced Patterns

Factory Functions

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.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 })),
};

Conditional Mocking

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.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("conditional API usage", async () => {
  const { fetchData } = await import("./api");
  const result = await fetchData();

  if (shouldUseMockApi) {
    expect(result.data).toBe("mocked");
  }
});

Mock Cleanup Patterns

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
import { afterEach, beforeEach } from "bun:test";

beforeEach(() => {
  // Set up common mocks
  mock.module("./logger", () => ({
    log: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
  }));
});

afterEach(() => {
  // Clean up all mocks
  mock.restore();
  mock.clearAllMocks();
});

Best Practices

Keep Mocks Simple

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
// Good: Simple, focused mock
const mockUserApi = {
  getUser: mock(async id => ({ id, name: "Test User" })),
};

// Avoid: Overly complex mock behavior
const complexMock = mock(input => {
  if (input.type === "A") {
    return processTypeA(input);
  } else if (input.type === "B") {
    return processTypeB(input);
  }
  // ... lots of complex logic
});

Use Type-Safe Mocks

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 })),
};

Test Mock Behavior

https://mintcdn.com/bun-1dd33a4e/JUhaF6Mf68z_zHyy/icons/typescript.svg?fit=max&auto=format&n=JUhaF6Mf68z_zHyy&q=85&s=7ac549adaea8d5487d8fbd58cc3ea35btest.ts
test("service calls API correctly", async () => {
  const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };

  const service = new UserService(mockApi);
  await service.getUser("123");

  // Verify the mock was called correctly
  expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
  expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});

Notes

Auto-mocking

Bun does not support the __mocks__ directory or auto-mocking. If this is blocking you from switching to Bun, file an issue.

ESM vs CommonJS

Module mocks have different implementations for ESM and CommonJS modules. For ES modules, Bun patches JavaScriptCore so it can override export values at runtime and update live bindings recursively.