Skip to main content
模拟对于测试至关重要,它允许你用受控的实现替换依赖项。Bun 提供了全面的模拟功能,包括函数模拟、间谍以及模块模拟。

基础函数模拟

使用 mock 函数创建模拟。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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 兼容性

或者,你也可以像在 Jest 中那样使用 jest.fn() 函数。它的行为完全相同。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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() 的返回值是一个带有附加属性的新函数。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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 }
//  ]

可用属性和方法

模拟函数实现了以下属性和方法:
属性/方法描述
mockFn.getMockName()返回模拟名称
mockFn.mock.calls每次调用的参数数组
mockFn.mock.results每次调用的返回值数组
mockFn.mock.instances每次调用的 this 上下文数组
mockFn.mock.contexts每次调用的 this 上下文数组
mockFn.mock.lastCall最近一次调用的参数
mockFn.mockClear()清空调用历史
mockFn.mockReset()清空调用历史且移除实现
mockFn.mockRestore()恢复原始实现
mockFn.mockImplementation(fn)设置新的实现
mockFn.mockImplementationOnce(fn)只为下一次调用设置实现
mockFn.mockName(name)设置模拟名称
mockFn.mockReturnThis()设置返回值为 this
mockFn.mockReturnValue(value)设置返回值
mockFn.mockReturnValueOnce(value)只为下一次调用设置返回值
mockFn.mockResolvedValue(value)设置解析(resolved)的 Promise 值
mockFn.mockResolvedValueOnce(value)只为下一次调用设置解析的 Promise 值
mockFn.mockRejectedValue(value)设置拒绝(rejected)的 Promise 值
mockFn.mockRejectedValueOnce(value)只为下一次调用设置拒绝的 Promise 值
mockFn.withImplementation(fn, callback)临时更改实现

实际示例

基础模拟用法

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { test, expect, mock } from "bun:test";

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

  // 调用模拟函数
  const result1 = mockFn(5);
  const result2 = mockFn(10);

  // 验证调用情况
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith(5);
  expect(mockFn).toHaveBeenLastCalledWith(10);

  // 检查返回结果
  expect(result1).toBe(10);
  expect(result2).toBe(20);

  // 检视调用历史
  expect(mockFn.mock.calls).toEqual([[5], [10]]);
  expect(mockFn.mock.results).toEqual([
    { type: "return", value: 10 },
    { type: "return", value: 20 },
  ]);
});

动态模拟实现

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { test, expect, mock } from "bun:test";

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

  // 设置不同实现
  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"); // 使用默认实现
});

异步模拟

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { test, expect, mock } from "bun:test";

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

  // 模拟解析的值
  asyncMock.mockResolvedValueOnce("first result");
  asyncMock.mockResolvedValue("default result");

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

  // 模拟拒绝的值
  const rejectMock = mock();
  rejectMock.mockRejectedValue(new Error("Mock error"));

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

使用 spyOn() 创建间谍

可以不替换函数实现而追踪其调用。使用 spyOn() 创建间谍;这些间谍可以用于 .toHaveBeenCalled().toHaveBeenCalledTimes()
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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);
});

高级间谍用法

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { test, expect, spyOn, afterEach } from "bun:test";

class UserService {
  async getUser(id: string) {
    // 原始实现
    return { id, name: `User ${id}` };
  }

  async saveUser(user: any) {
    // 原始实现
    return { ...user, saved: true };
  }
}

const userService = new UserService();

afterEach(() => {
  // 每个测试后恢复所有间谍
  jest.restoreAllMocks();
});

test("spy on service methods", async () => {
  // 监听但不改变实现
  const getUserSpy = spyOn(userService, "getUser");
  const saveUserSpy = spyOn(userService, "saveUser");

  // 正常使用服务
  const user = await userService.getUser("123");
  await userService.saveUser(user);

  // 验证调用
  expect(getUserSpy).toHaveBeenCalledWith("123");
  expect(saveUserSpy).toHaveBeenCalledWith(user);
});

test("spy with mock implementation", async () => {
  // 监听并覆盖实现
  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");
});

使用 mock.module() 进行模块模拟

模块模拟允许覆盖模块行为。使用 mock.module(path: string, callback: () => Object) 来模拟模块。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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");
});
与 Bun 其他部分一样,模块模拟同时支持 importrequire

重写已导入模块

如果你需要重写已经导入的模块,无需额外操作。调用 mock.module() 即可覆盖模块。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { test, expect, mock } from "bun:test";

// 我们将模拟的模块:
import { foo } from "./module";

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

  // 这里更新模拟:
  mock.module("./module", () => {
    return {
      foo: "baz",
    };
  });

  // 实时绑定会自动更新。
  expect(foo).toBe("baz");

  // 该模块对 CJS 也会更新。
  expect(cjs.foo).toBe("baz");
});

提前声明与预加载

如果你需要确保模块在导入前被模拟,应使用 --preload 在测试运行前加载模拟。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79my-preload.ts
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
terminal
bun test --preload ./my-preload
为了方便,可以将预加载写入你的 bunfig.toml
bunfig.toml
[test]
# 在运行测试前加载这些模块。
preload = ["./my-preload"]

模块模拟最佳实践

何时使用预加载

如果模拟的模块已被导入,会发生什么? 如果模拟了已导入的模块,模块缓存中会更新该模块。这意味着导入它的模块将得到模拟版本,但原始模块已经执行过了,相关副作用已发生。 如果你想阻止原模块执行,应使用 --preload 在测试前加载模拟。

实际模块模拟示例

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79api-client.test.ts
import { test, expect, mock, beforeEach } from "bun:test";

// 模拟 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("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");
});

模拟外部依赖

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79database.test.ts
import { test, expect, mock } from "bun:test";

// 模拟外部数据库库
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");
});

全局模拟函数

清除所有模拟

重置所有模拟函数状态(调用、结果等),但不还原它们的原始实现:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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);

  // 注意:实现依然存在
  expect(typeof random1()).toBe("number");
  expect(typeof random2()).toBe("number");
});
这会重置所有模拟的 .mock.calls.mock.instances.mock.contexts.mock.results,但不像 mock.restore() 那样恢复原始实现。

恢复所有模拟

与逐个调用 mockFn.mockRestore() 不同,可用 mock.restore() 一次性恢复所有模拟。此操作不会重置通过 mock.module() 覆盖的模块值。
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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");

  // 原始实现仍可用
  expect(fooModule.foo()).toBe("foo");
  expect(barModule.bar()).toBe("bar");
  expect(bazModule.baz()).toBe("baz");

  // 模拟实现
  fooSpy.mockImplementation(() => 42);
  barSpy.mockImplementation(() => 43);
  bazSpy.mockImplementation(() => 44);

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

  // 恢复所有
  mock.restore();

  expect(fooModule.foo()).toBe("foo");
  expect(barModule.bar()).toBe("bar");
  expect(bazModule.baz()).toBe("baz");
});
mock.restore() 放入每个测试文件的 afterEach 钩子或测试预加载代码中,可以减少测试代码量。

Vitest 兼容性

为方便移植 Vitest 编写的测试,Bun 提供了 vi 对象作为 Jest 模拟 API 部分的别名:
https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { test, expect, vi } from "bun:test";

// 使用 'vi' 别名,类似 Vitest
test("vitest compatibility", () => {
  const mockFn = vi.fn(() => 42);

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

  // vi 对象支持以下函数:
  // vi.fn
  // vi.spyOn
  // vi.mock
  // vi.restoreAllMocks
  // vi.clearAllMocks
});
这样可以更轻松地将 Vitest 测试迁移到 Bun,无需重写所有模拟。

实现细节

了解 mock.module() 的工作原理可帮助更有效地使用:

缓存交互

模块模拟会与 ESM 和 CommonJS 模块缓存交互。

惰性求值

模拟工厂回调只有在模块真正被导入或 require 时才会被执行。

路径解析

Bun 会自动解析模块标识符,支持:
  • 相对路径('./module'
  • 绝对路径('/path/to/module'
  • 包名('lodash'

导入时机影响

  • 在首次导入前模拟:原模块不会执行副作用
  • 在导入后模拟:原模块副作用已经发生
因此,建议使用 --preload 来防止副作用。

实时绑定

模拟的 ESM 模块保持实时绑定,因此修改模拟会更新所有已有导入。

高级模式

工厂函数

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { mock } from "bun:test";

function createMockUser(overrides = {}) {
  return {
    id: "mock-id",
    name: "Mock User",
    email: "[email protected]",
    ...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 })),
};

条件式模拟

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.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");
  }
});

模拟清理模式

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
import { afterEach, beforeEach } from "bun:test";

beforeEach(() => {
  // 设定通用模拟
  mock.module("./logger", () => ({
    log: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
  }));
});

afterEach(() => {
  // 清理所有模拟
  mock.restore();
  mock.clearAllMocks();
});

最佳实践

保持模拟简单

https://mintcdn.com/ikxin/RzFFGbzo0-4huILA/icons/typescript.svg?fit=max&auto=format&n=RzFFGbzo0-4huILA&q=85&s=a3dffd2241f05776d3bd25171d0c5a79test.ts
// 好:简单、聚焦的模拟
const mockUserApi = {
  getUser: mock(async id => ({ id, name: "Test User" })),
};

// 避免:过于复杂的模拟行为
const complexMock = mock(input => {
  if (input.type === "A") {
    return processTypeA(input);
  } else if (input.type === "B") {
    return processTypeB(input);
  }
  // ...很多复杂逻辑
});

使用类型安全的模拟

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

测试模拟行为

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

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

  // 验证模拟被正确调用
  expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
  expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});

备注

自动模拟

目前不支持 __mocks__ 目录和自动模拟。如此阻碍你迁移到 Bun,请提交 issue

ESM 与 CommonJS

模块模拟对 ESM 和 CommonJS 模块实现不同。对于 ES 模块,Bun 对 JavaScriptCore 进行补丁,允许运行时覆盖导出值,并递归更新实时绑定。