Skip to content

6.2 Node 环境与 Vitest 单元测试 ​

MSW 最大的价值之一:浏览器开发用的那套 handler,在 Node 测试里原样运行。没有 vi.mock,没有接口适配层,测试发的也是真实的 fetch。

setupServer ​

ts
// tests/server.ts
import { setupServer } from "msw/node";
import { handlers } from "../src/mocks/handlers";

export const server = setupServer(...handlers);

Vitest 的 setup 文件管理生命周期:

ts
// vitest.setup.ts
import { server } from "./tests/server";

beforeAll(() => server.listen({ onUnhandledFrame: "error" })); // 未声明的请求一律失败
afterEach(() => server.resetHandlers());                       // 用例间隔离
afterAll(() => server.close());
ts
// vitest.config.ts
export default defineConfig({
  test: {
    environment: "node", // 测纯逻辑/服务代码用 node;测组件时用 jsdom / happy-dom
    setupFiles: ["./vitest.setup.ts"],
  },
});

测试组件时的三种数据策略 ​

ts
import { http, HttpResponse } from "msw";
import { server } from "./tests/server";

test("成功态:渲染用户列表", async () => {
  server.use(
    http.get("*/api/users", () =>
      HttpResponse.json({ data: [{ id: "1", name: "小邮差" }], total: 1 }),
    ),
  );
  render(<UserList />);
  expect(await screen.findByText("小邮差")).toBeInTheDocument();
});

test("空态:渲染暂无数据", async () => {
  server.use(http.get("*/api/users", () => HttpResponse.json({ data: [], total: 0 })));
  render(<UserList />);
  expect(await screen.findByText("暂无数据")).toBeInTheDocument();
});

test("错误态:500 时显示重试按钮", async () => {
  server.use(http.get("*/api/users", () => HttpResponse.json({ error: {} }, { status: 500 })));
  render(<UserList />);
  expect(await screen.findByRole("button", { name: "重试" })).toBeInTheDocument();
});

test("加载态:慢接口下 loading 不闪烁", async () => {
  server.use(
    http.get("*/api/users", async () => {
      await delay(800);
      return HttpResponse.json({ data: [], total: 0 });
    }),
  );
  render(<UserList />);
  expect(screen.getByText("加载中…")).toBeInTheDocument();
});

关键模式:默认 handler 提供"快乐路径",个别用例用 server.use() 覆盖,afterEach(resetHandlers) 保证用例互不污染。

断言"客户端发了什么" ​

除了断言 UI,还可以断言请求本身(无需 mock 函数):

ts
import { http, HttpResponse } from "msw";

test("提交表单时发送了正确的载荷", async () => {
  let receivedBody: CreateUserInput | null = null;
  server.use(
    http.post("*/api/users", async ({ request }) => {
      receivedBody = (await request.json()) as CreateUserInput;
      return HttpResponse.json({ user: { id: "1", ...receivedBody } }, { status: 201 });
    }),
  );

  render(<CreateUserForm />);
  await userEvent.type(screen.getByLabelText("姓名"), "小邮差");
  await userEvent.click(screen.getByRole("button", { name: "保存" }));

  await waitFor(() => expect(receivedBody).toMatchObject({ name: "小邮差", role: "member" }));
});

浏览器与 Node 的两个差异(重要) ​

  1. 路径必须带通配源:http.get("*/api/users") 在两端都工作;相对路径 /api/users 在 Node 里不匹配(没有 origin)。
  2. localStorage 等 Web API:Node 环境不存在。本仓库的假数据库在 Node 下自动退化为内存种子,保证每个测试进程数据确定。

本仓库的测试实践 ​

packages/mocks 直接对 handler 层做单测 —— 启动 server 后用真实 fetch 打每个端点, 覆盖正常/异常/边界。这是"单一真相源"架构的额外红利:handler 自身有测试,应用复用的就是被测试过的契约。

bash
pnpm --filter @msw-tutorial/mocks test           # 41 个用例
pnpm --filter @msw-tutorial/mocks test:coverage  # 覆盖率(行覆盖 90%+)

Vitest setup 文件示例下载