模拟(Mock)
基于 Vitest v4.1.x 编写
速查
- 造函数:
vi.fn(),.mockReturnValue()/.mockResolvedValue()/.mockImplementation()/*Once - 监视方法:
vi.spyOn(obj, "method"),.mockRestore()恢复;v4 可用using自动恢复 - 模块 mock:
vi.mock(path, factory)——会被提升到文件顶部 - 提升变量:
vi.hoisted(() => ({...})),解决 factory 引用外部变量报错 - 动态 mock:
vi.doMock(不提升)+ 动态import() - 类型安全:
vi.mocked(fn).mockReturnValue(...) - 全局 / 环境:
vi.stubGlobal()/vi.stubEnv(),配unstubAllGlobals/unstubAllEnvs - 假定时器:
vi.useFakeTimers()+advanceTimersByTime()/runAllTimers()/setSystemTime()
vi.fn — 造一个 mock 函数
import { vi, expect, test } from "vitest";
test("记录调用 + 设定返回", () => {
const fn = vi.fn().mockReturnValue("Hi");
expect(fn("a", 1)).toBe("Hi");
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith("a", 1);
});
// 一次性返回、自定义实现、异步
const once = vi.fn().mockReturnValueOnce("first").mockReturnValue("default");
const add = vi.fn((a: number, b: number) => a + b);
const fetchUser = vi.fn().mockResolvedValue({ id: 1 });vi.spyOn — 监视已有方法
import { vi, expect, test } from "vitest";
const utils = { now: () => Date.now() };
test("替换实现并断言调用", () => {
const spy = vi.spyOn(utils, "now").mockReturnValue(1000);
expect(utils.now()).toBe(1000);
expect(spy).toHaveBeenCalled();
spy.mockRestore(); // 恢复原实现
});v4 可用 ES 显式资源管理 using,离开作用域自动 mockRestore:
test("自动恢复", () => {
using spy = vi.spyOn(console, "log").mockImplementation(() => {});
console.log("x");
expect(spy).toHaveBeenCalled();
}); // 这里自动 restore,不必手写 afterEach监视 getter / setter:
const spy = vi.spyOn(obj, "count", "get").mockReturnValue(42);vi.mock — 模块级 mock 与提升
vi.mock 是 Vitest 最容易踩坑、也最高频考的点:它会被插件提升(hoist)到文件所有 import 之前执行,所以写在哪一行都一样早。
import { getUser } from "./api"; // 实际拿到的是被 mock 后的版本
vi.mock("./api", () => ({
getUser: vi.fn().mockResolvedValue({ id: 1, name: "Alice" }),
}));保留部分原实现,用 importOriginal:
vi.mock("./utils", async (importOriginal) => {
const original = await importOriginal<typeof import("./utils")>();
return {
...original,
formatDate: vi.fn().mockReturnValue("2026-01-01"), // 只改这一个
};
});推荐用“动态 import 形式”,IDE 类型推断更好:
vi.mock(import("./module"), async (importOriginal) => {
const mod = await importOriginal(); // 类型自动推断
return { ...mod, total: vi.fn() };
});factory 里不能直接用外部变量
因为提升,下面的 MOCK 在 factory 执行时还未定义:
const MOCK = "test";
vi.mock("./mod", () => ({ value: MOCK })); // ❌ ReferenceError必须用 vi.hoisted 把变量也提上去(见下)。
vi.hoisted — 把变量提到 mock 之前
const mocks = vi.hoisted(() => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1 }),
}));
vi.mock("./api", () => ({
fetchUser: mocks.fetchUser,
}));
test("可在用例里改 mock 行为", async () => {
mocks.fetchUser.mockResolvedValueOnce({ id: 99 });
// ...
});vi.doMock — 不提升、按条件 mock
需要“运行到某一刻才决定怎么 mock”时用 vi.doMock,它不提升,但因此只能配合动态 import() 生效:
test("条件 mock", async () => {
vi.doMock("./config", () => ({ env: "test" }));
const { env } = await import("./config"); // 必须动态 import
expect(env).toBe("test");
vi.doUnmock("./config");
});vi.mocked — 类型安全包装
vi.mock 后,导入的函数运行时是 mock,但 TS 仍认为是原类型。用 vi.mocked 拿到 mock 的方法提示:
import * as api from "./api";
vi.mock("./api");
test("有 .mockResolvedValue 提示", () => {
vi.mocked(api.getUser).mockResolvedValue({ id: 1, name: "Alice" });
vi.mocked(api.getConfig, { partial: true }).mockReturnValue({ debug: true });
});vi.mock 的 spy 模式(v4)
{ spy: true }:automock 但保留原始实现,同时记录调用——既测真实逻辑又能断言被调用:
import { calculator } from "./calculator";
vi.mock("./calculator", { spy: true });
test("跑真实现 + 断言调用", () => {
expect(calculator(1, 2)).toBe(3); // 真实结果
expect(calculator).toHaveBeenCalledWith(1, 2);
});mocks 目录
与 Jest 不同,Vitest 不自动加载 __mocks__/ 下的手动 mock;必须显式 vi.mock("模块名"),它才会去找同名 mock 文件:
vi.mock("axios"); // 此时才使用 __mocks__/axios.ts模拟全局与环境变量
import { vi, afterEach } from "vitest";
// 补 jsdom 缺失的全局(组件测试常用)
vi.stubGlobal(
"IntersectionObserver",
vi.fn(() => ({ observe: vi.fn(), disconnect: vi.fn(), unobserve: vi.fn() })),
);
// 改环境变量(同时作用于 process.env 和 import.meta.env)
vi.stubEnv("NODE_ENV", "production");
afterEach(() => {
vi.unstubAllGlobals();
vi.unstubAllEnvs();
});
// 或配置 unstubGlobals: true / unstubEnvs: true 自动清理假定时器
把 setTimeout / setInterval / Date 等交给 Vitest 控制,让“等 1 秒”变成同步推进:
import { vi, beforeEach, afterEach, expect, test } from "vitest";
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
test("推进时间触发回调", () => {
const cb = vi.fn();
setTimeout(cb, 1000);
vi.advanceTimersByTime(1000); // 直接快进,不必真的等
expect(cb).toHaveBeenCalledTimes(1);
});
test("固定系统时间", () => {
vi.setSystemTime(new Date("2026-01-01T00:00:00Z"));
expect(new Date().getFullYear()).toBe(2026);
});常用推进方法:advanceTimersByTime(ms)、runAllTimers()、advanceTimersToNextTimer(),以及含 Promise 的异步版 advanceTimersByTimeAsync()。
两个高频坑
- factory 引用外部变量报错 → 用
vi.hoisted(见上)。 - 同文件内部调用无法被 mock:
// utils.ts
export function helper() { return 42; }
export function caller() { return helper(); } // 直接调用,不经过模块导出vi.mock("./utils");
vi.mocked(helper).mockReturnValue(99);
caller(); // 仍是 42——内部引用绕过了 mock解决:把 helper 拆到独立文件,或改用依赖注入。
清理策略(clearMocks / resetMocks / restoreMocks)见 配置。