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

# 模拟

> 了解如何在 Bun 测试中创建和使用模拟函数、间谍和模块模拟

模拟用受控实现替换依赖。Bun 支持函数模拟、间谍和模块模拟。

## 基本函数模拟

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

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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`                   | 使用 `new` 创建的实例数组      |
| `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)`         | 设置已解决的 Promise 值      |
| `mockFn.mockResolvedValueOnce(value)`     | 仅为下一次调用设置已解决的 Promise |
| `mockFn.mockRejectedValue(value)`         | 设置已拒绝的 Promise 值      |
| `mockFn.mockRejectedValueOnce(value)`     | 仅为下一次调用设置已拒绝的 Promise |
| `mockFn.withImplementation(fn, callback)` | 临时更改实现                |

### 实用示例

#### 基本模拟用法

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { test, expect, mock } from "bun:test";

test("模拟函数行为", () => {
  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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { test, expect, mock } from "bun:test";

test("动态模拟实现", () => {
  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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { test, expect, mock } from "bun:test";

test("异步模拟函数", 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("模拟错误"));

  await expect(rejectMock()).rejects.toThrow("模拟错误");
});
```

## 使用 spyOn() 进行间谍活动

使用 `spyOn()` 跟踪对函数的调用，而不将其替换为模拟。间谍可以传递给 `.toHaveBeenCalled()` 和 `.toHaveBeenCalledTimes()`。

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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("间谍服务方法", 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("带模拟实现的间谍", async () => {
  // 进行间谍活动并覆盖实现
  const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
    id: "123",
    name: "模拟用户",
  });

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

  expect(result.name).toBe("模拟用户");
  expect(getUserSpy).toHaveBeenCalledWith("123");
});
```

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

使用 `mock.module(path: string, callback: () => Object)` 覆盖模块的行为。

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { mock } from "bun:test";

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

```bash terminal icon="terminal" theme={null}
bun test --preload ./my-preload
```

为了避免每次运行测试时都输入 `--preload`，将其添加到你的 `bunfig.toml`：

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

### 模块模拟最佳实践

#### 何时使用预加载

模拟已导入的模块会更新模块缓存，因此任何导入它的内容都会获得模拟版本。但原始模块已经被求值，因此其副作用已经发生。

为防止原始模块被求值，请使用 `--preload` 在测试运行之前加载你的模拟。

#### 实用模块模拟示例

```ts title="api-client.test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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("使用模拟 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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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: "测试用户" }],
      })),
      end: mock(async () => {}),
    };
  }),
}));

test("数据库操作", async () => {
  const { Database } = await import("./database");
  const db = new Database();

  const users = await db.getUsers();
  expect(users).toHaveLength(1);
  expect(users[0].name).toBe("测试用户");
});
```

## 全局模拟函数

### 清除所有模拟

`mock.clearAllMocks()` 会重置每个模拟的 `.mock.calls`、`.mock.instances`、`.mock.contexts` 和 `.mock.results` 属性。与 `mock.restore()` 不同，它不会恢复原始实现：

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { expect, mock, test } from "bun:test";

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

test("清除所有模拟", () => {
  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");
});
```

### 重置所有模拟

`jest.resetAllMocks()`（及其别名 `vi.resetAllMocks()`）在每个模拟上调用 `mockFn.mockReset()`：除了 `clearAllMocks()` 所做的事情外，它还会丢弃由 `mockImplementation()`、`mockReturnValue()` 等设置的实现。它不会恢复间谍的原始实现：

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { expect, jest, test } from "bun:test";

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

test("重置所有模拟", () => {
  random();
  expect(random).toHaveBeenCalledTimes(1);

  jest.resetAllMocks();

  expect(random).toHaveBeenCalledTimes(0);
  // 与 clearAllMocks() 不同，实现已消失
  expect(random()).toBeUndefined();
});
```

### 恢复所有模拟

`mock.restore()` 一次性恢复所有模拟，而不是在每个模拟上调用 `mockFn.mockRestore()`。它不会重置通过 `mock.module()` 覆盖的模块。

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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");
});
```

在 `afterEach` 块或测试预加载脚本中调用 `mock.restore()`，而不是在每次测试中重复清理。

## Vitest 兼容性

为了与为 Vitest 编写的测试增加兼容性，Bun 提供了 `vi` 对象作为 Jest 模拟 API 部分功能的别名：

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { test, expect, vi } from "bun:test";

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

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

  // vi 对象上可用的以下函数：
  // vi.fn
  // vi.spyOn
  // vi.mock
  // vi.restoreAllMocks
  // vi.resetAllMocks
  // vi.clearAllMocks
});
```

你可以在不重写模拟的情况下从 Vitest 移植测试。

## 实现细节

### 缓存交互

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

### 惰性求值

模拟工厂回调仅当模块被导入或 require 时才被求值。

### 路径解析

Bun 解析模块说明符的方式与解析 `import` 的方式相同，支持：

* 相对路径（`'./module'`）
* 绝对路径（`'/path/to/module'`）
* 包名（`'lodash'`）

### 导入时机影响

* **在首次导入前模拟**：不会产生原始模块的副作用
* **在导入后模拟**：原始模块的副作用已发生

因此，对于需要防止副作用的模拟，请使用 `--preload`。

### 实时绑定

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

## 高级模式

### 工厂函数

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
import { mock } from "bun:test";

function createMockUser(overrides = {}) {
  return {
    id: "mock-id",
    name: "模拟用户",
    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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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("条件性 API 使用", 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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
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/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
// 好：简单、专注的模拟
const mockUserApi = {
  getUser: mock(async id => ({ id, name: "测试用户" })),
};

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

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

```ts theme={null}
interface UserService {
  getUser(id: string): Promise<User>;
  createUser(data: CreateUserData): Promise<User>;
}

const mockUserService: UserService = {
  getUser: mock(async (id: string) => ({ id, name: "测试用户" })),
  createUser: mock(async data => ({ id: "new-id", ...data })),
};
```

### 测试模拟行为

```ts title="test.ts" icon="https://mintcdn.com/span-inc/82N53aP7NFbaCVSl/icons/typescript.svg?fit=max&auto=format&n=82N53aP7NFbaCVSl&q=85&s=787d39d6a7d96d7f9540dc74344eba23" theme={null}
test("服务正确调用 API", 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);
});
```

## 注意

### 自动模拟

Bun 不支持 `__mocks__` 目录或自动模拟。如果这阻止了你切换到 Bun，请[提交 issue](https://github.com/oven-sh/bun/issues)。

### ESM 与 CommonJS

模块模拟在 ESM 和 CommonJS 模块中有不同的实现。对于 ES 模块，Bun 会修补 JavaScriptCore，以便在运行时覆盖导出值并递归更新实时绑定。
