@ 前端测试/ server- stubs
解开MSW为Vitest所打的钩. 单线JSON反应 与内置请求间谍相嘲弄.
特征
- 🎯 零沸腾 - 任何端口都用自动请求捕获
- 🔍 内建间谍 - 用解析后的身体、标题和参数跟踪所有请求
- 🔄 顺序答复 - 模拟重试、授标或状态变化
- 🎭 动态反应 - 根据请求数据计算答复
- 📦 类型脚本 - 全类型支持
安装
npm install -D @frontend-testing/server-stubs
同伴依赖关系 : msw >= 2.0.0, (中文(简体) ), vitest >= 3.0.0
快速启动
1. 配置MSW 服务器
// src/test/setup.ts
import { setupWorker } from "msw/browser"; // or setupServer from "msw/node"
import { serverManager } from "@frontend-testing/server-stubs";
import { beforeAll, afterEach, afterAll } from "vitest";
const server = setupWorker();
serverManager.setDefaultServerLoader(() => server);
beforeAll(() => server.start({ onUnhandledRequest: "warn" }));
afterEach(() => server.resetHandlers());
afterAll(() => server.stop());
2. 测试中的使用
import { stubJsonResponse } from "@frontend-testing/server-stubs";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
describe("LoginForm", () => {
it("sends credentials and handles success", async () => {
const { spy } = stubJsonResponse({
path: "*/api/auth/login",
method: "POST",
response: { token: "jwt-token", user: { id: 1, name: "John" } },
});
render(<LoginForm />);
await userEvent.type(screen.getByLabelText("Email"), "[email protected]");
await userEvent.type(screen.getByLabelText("Password"), "secret123");
await userEvent.click(screen.getByRole("button", { name: "Login" }));
expect(spy).toHaveBeenCalledTimes(1);
expect(spy.mock.calls[0][0].body).toEqual({
email: "[email protected]",
password: "secret123",
});
});
it("displays error on 401", async () => {
stubJsonResponse({
path: "*/api/auth/login",
method: "POST",
response: { error: "Invalid credentials" },
status: 401,
});
render(<LoginForm />);
await userEvent.click(screen.getByRole("button", { name: "Login" }));
await screen.findByText("Invalid credentials");
});
});
API 密码
stubJsonResponse(options)
创建一个能截取匹配请求并返回 JSON 响应的处理器 .
const { spy } = stubJsonResponse({
path: "*/api/users", // URL pattern (supports MSW wildcards)
method: "POST", // GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, ALL
response: { id: 1 }, // Static value or (ctx) => value
status: 200, // Static number or (ctx) => number
});
| 选项 | 类型 | 默认 | 说明 |
|---|---|---|---|
path | string | 所需经费 | 要匹配的 URL 模式 |
method | HttpMethod | "GET" | HTTP 方法 |
response | unknown | (ctx) => unknown | undefined | 反应机构 |
status | number | (ctx) => number | 200 | HTTP 状态代码 |
responses | SequentialResponse[] | - 说吧 | 顺序答复 |
请求间谍
每个树枝都回了 spy 获取请求的细节 :
const { spy } = stubJsonResponse({ path: "*/api/users", response: [] });
await fetch("/api/users?page=1", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "John" }),
});
const call = spy.mock.calls[0][0];
call.url; // Full request URL
call.method; // "POST"
call.body; // { name: "John" } (auto-parsed)
call.headers; // { "content-type": "application/json", ... }
call.params; // { id: "123" } for /users/:id patterns
call.request; // Original Request object
图像分析
请求机构根据 Content-Type数字 :
| 内容 - 类型 | 解析为 |
|---|---|
application/json | JavaScript 对象 |
application/x-www-form-urlencoded | 来自密钥值对的对象 |
multipart/form-data | 对象为 File 实例 |
| 无信头/ 未知 | 尝试 JSON, 返回到原始文本 |
动态反应
使用函数根据请求数据计算响应:
stubJsonResponse({
path: "*/api/echo",
method: "POST",
response: (ctx) => ({ received: ctx.body }),
status: (ctx) => ctx.body?.valid ? 200 : 400,
});
回应背景:
interface ResponseContext {
url: string;
method: string;
body?: unknown;
headers: Record<string, string>;
params?: Record<string, string>;
callIndex: number; // 0-based call counter
}
顺序答复
模拟多个调用中的逻辑或状态变化 :
stubJsonResponse({
path: "*/api/flaky",
responses: [
{ response: { error: "Service unavailable" }, status: 503 },
{ response: { error: "Service unavailable" }, status: 503 },
{ response: { data: "Success!" }, status: 200 },
],
});
// 1st call → 503
// 2nd call → 503
// 3rd+ calls → 200 (repeats last response)
serverManager
Singleton)用于MSW服务器管理. 配置一次,在任何地方使用根茎.
import { serverManager } from "@frontend-testing/server-stubs";
// Set server directly
serverManager.setServer(myServer);
// Or lazy load (recommended)
serverManager.setDefaultServerLoader(() => myServer);
// Access
serverManager.getServer();
serverManager.hasServer();
serverManager.reset();
高级示例
文件上传
const { spy } = stubJsonResponse({
path: "*/api/upload",
method: "POST",
response: { id: "file-123" },
});
const formData = new FormData();
formData.append("file", new File(["content"], "doc.pdf"));
await fetch("/api/upload", { method: "POST", body: formData });
const body = spy.mock.calls[0][0].body as Record<string, unknown>;
expect(body.file).toBeInstanceOf(File);
抓好所有处理器
stubJsonResponse({
path: "*/api/flexible",
method: "ALL",
response: (ctx) => ({ method: ctx.method }),
});
多个终点
const usersSpy = stubJsonResponse({
path: "*/api/users",
response: [{ id: 1, name: "John" }],
});
const statsSpy = stubJsonResponse({
path: "*/api/stats",
response: { visits: 1000 },
});
类型脚本
所有类型都导出 :
import type {
HttpMethod,
RequestCall,
ResponseContext,
ResponseProvider,
SequentialResponse,
StubJsonResponseOptions,
MswServer,
} from "@frontend-testing/server-stubs";
如何运作
这个库将MSW的请求处理器用更清洁的API和自动请求跟踪来包裹. 当你打电话 stubJsonResponse(),则:
- 为指定的路径/方法创建 MSW 处理器
- 根据
Content-Type - 记录所有向Vitest间谍提出的请求
- 返回所配置的响应(静态、动态或相继)
这个 serverManager 从测试文件中去除MSW服务器设置, 因此您会在测试设置中配置一次, 在不导入服务器实例的情况下, 在任何地方使用 stubs .
许可证
麻省理工学院