> ## Documentation Index
> Fetch the complete documentation index at: https://bun.zhcndoc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Mock

> 学习如何在 Bun 测试中创建和使用模拟函数、间谍以及模块模拟

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

## 基础函数模拟

使用 `mock` 函数创建模拟。

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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()` 函数。它的行为完全相同。

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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()` 的返回值是一个带有附加属性的新函数。

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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)` | 临时更改实现                    |

### 实际示例

#### 基础模拟用法

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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 },
  ]);
});
```

#### 动态模拟实现

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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"); // 使用默认实现
});
```

#### 异步模拟

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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()`。

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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);
});
```

### 高级间谍用法

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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)` 来模拟模块。

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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 其他部分一样，模块模拟同时支持 `import` 和 `require`。

### 重写已导入模块

如果你需要重写已经导入的模块，无需额外操作。调用 `mock.module()` 即可覆盖模块。

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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` 在测试运行前加载模拟。

```ts title="my-preload.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
```

```bash terminal icon="terminal" theme={"theme":{"light":"github-light","dark":"dracula"}}
bun test --preload ./my-preload
```

为了方便，可以将预加载写入你的 `bunfig.toml`：

```toml title="bunfig.toml" icon="settings" theme={"theme":{"light":"github-light","dark":"dracula"}}
[test]
# 在运行测试前加载这些模块。
preload = ["./my-preload"]
```

### 模块模拟最佳实践

#### 何时使用预加载

**如果模拟的模块已被导入，会发生什么？**

如果模拟了已导入的模块，模块缓存中会更新该模块。这意味着导入它的模块将得到模拟版本，但原始模块已经执行过了，相关副作用已发生。

如果你想阻止原模块执行，应使用 `--preload` 在测试前加载模拟。

#### 实际模块模拟示例

```ts title="api-client.test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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");
});
```

#### 模拟外部依赖

```ts title="database.test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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");
});
```

## 全局模拟函数

### 清除所有模拟

重置所有模拟函数状态（调用、结果等），但不还原它们的原始实现：

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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()` 覆盖的模块值。

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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 部分的别名：

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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 模块保持实时绑定，因此修改模拟会更新所有已有导入。

## 高级模式

### 工厂函数

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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 })),
};
```

### 条件式模拟

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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");
  }
});
```

### 模拟清理模式

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
import { afterEach, beforeEach } from "bun:test";

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

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

## 最佳实践

### 保持模拟简单

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
// 好：简单、聚焦的模拟
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);
  }
  // ...很多复杂逻辑
});
```

### 使用类型安全的模拟

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
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 })),
};
```

### 测试模拟行为

```ts title="test.ts" icon="https://mintcdn.com/bun-zhcndoc/cnUTwgMuf4cCrwC-/icons/typescript.svg?fit=max&auto=format&n=cnUTwgMuf4cCrwC-&q=85&s=e7767043c9e885c34f2d6c8fe2a95217" theme={"theme":{"light":"github-light","dark":"dracula"}}
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](https://github.com/oven-sh/bun/issues)。

### ESM 与 CommonJS

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