news 2026/9/10 7:23:15

如何为 bulletproof-react 的 MSW mock API 定义数据模型并生成测试数据?

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何为 bulletproof-react 的 MSW mock API 定义数据模型并生成测试数据?

如何为 bulletproof-react 的 MSW mock API 定义数据模型并生成测试数据?

【免费下载链接】bulletproof-react🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications.项目地址: https://gitcode.com/GitHub_Trending/bu/bulletproof-react

bulletproof-react 的apps/react-vite应用用 MSW(Mock Service Worker)在开发阶段模拟 API:在 service worker 里拦截所有 HTTP 请求,返回 handler 定义好的响应。要让 MSW 返回"像真后端一样"的数据,需要同时准备两块:

  • 数据模型@mswjs/datafactory):声明每个资源的字段、主键、类型,作为 handler 里的查询入口。
  • 测试数据生成器@ngneat/falso):为每个模型产出一份默认填充好的随机对象,支持字段覆盖,便于快速造数据。

本文以apps/react-vite为例,把这条路径串成可执行流程:定义模型 → 生成数据 → 让 handler 消费 → 启动 MSW → 验证数据确实进入 mock API。

1. 依赖清单(项目中已就位)

apps/react-vite/package.json中与本任务直接相关的依赖:

  • msw^2.2.14):拦截器本体
  • @mswjs/data^0.16.1):模型工厂
  • @mswjs/http-middleware^0.10.1):把同一批 handler 挂到 Express,用于 E2E mock server
  • nanoid^5.0.7):模型主键生成器
  • @ngneat/falso^7.2.0):随机数据生成器

apps/react-vite下执行yarn install后这些包即可直接引入使用。

2. 定义数据模型(db.ts)

模型定义集中在 db.ts。核心是@mswjs/datafactoryprimaryKey

import { factory, primaryKey } from '@mswjs/data'; import { nanoid } from 'nanoid'; const models = { user: { id: primaryKey(nanoid), firstName: String, lastName: String, email: String, password: String, teamId: String, role: String, bio: String, createdAt: Date.now, }, team: { id: primaryKey(nanoid), name: String, description: String, createdAt: Date.now, }, discussion: { id: primaryKey(nanoid), title: String, body: String, authorId: String, teamId: String, createdAt: Date.now, }, comment: { id: primaryKey(nanoid), body: String, authorId: String, discussionId: String, createdAt: Date.now, }, }; export const db = factory(models); export type Model = keyof typeof models;

字段约定:

  • id: primaryKey(nanoid)—— 主键,由nanoid()生成;其余字段以StringDate.now等标注,@mswjs/data据此做类型校验与默认值。
  • export type Model = keyof typeof models—— 后续persistDb按模型名持久化时使用这个联合类型。

2.1 持久化与初始化

同一文件还导出四个函数,handler 与测试都依赖它们:

函数行为
loadDb()Node.js 环境读mocked-db.json(不存在时写空{});浏览器环境读localStorage['msw-db']
storeDb(data)把 JSON 写回对应位置
persistDb(model: Model)把指定模型当前内存中全部行(db[model].getAll())合并进持久化数据;NODE_ENV === 'test'时直接跳过,测试不写盘
initializeDb()把持久化数据逐条model.create(entry)灌回db
resetDb()window.localStorage.clear(),即只清理浏览器/测试环境的存储

3. 生成测试数据(data-generators.ts)

data-generators.ts 用@ngneat/falso为每个模型提供一个默认填充函数,并对外导出一个带overrides的工厂:

import { randCompanyName, randUserName, randEmail, randParagraph, randUuid, randPassword, randCatchPhrase, } from '@ngneat/falso'; const generateUser = () => ({ id: randUuid() + Math.random(), firstName: randUserName({ withAccents: false }), lastName: randUserName({ withAccents: false }), email: randEmail(), password: randPassword(), teamId: randUuid(), teamName: randCompanyName(), role: 'ADMIN', bio: randParagraph(), createdAt: Date.now(), }); export const createUser = <T extends Partial<ReturnType<typeof generateUser>>>( overrides?: T, ) => { return { ...generateUser(), ...overrides }; };

文件里依次导出createUsercreateTeamcreateDiscussioncreateComment。用法固定为"默认 + 覆盖":

import { createUser, createDiscussion } from '@/testing/data-generators'; const user = createUser({ email: 'seed@example.com' }); // => { id: '…', email: 'seed@example.com', firstName: '…', ... }

字段名与db.ts中的模型字段一一对应,生成出的对象可以直接交给db.user.create(...)写入。

4. 让 handler 消费模型与生成数据

Handler 位于 handlers/ 目录,按资源拆分为auth.ts/users.ts/teams.ts/discussions.ts/comments.ts,在 index.ts 中合并为统一数组:

export const handlers = [ ...authHandlers, ...commentsHandlers, ...discussionsHandlers, ...teamsHandlers, ...usersHandlers, http.get(`${env.API_URL}/healthcheck`, async () => { await networkDelay(); return HttpResponse.json({ ok: true }); }), ];

以 discussions.ts 为例,handler 通过db直接读写模型:

import { db, persistDb } from '../db'; // 查询:按 teamId 过滤并分页 const total = db.discussion.count({ where: { teamId: { equals: user?.teamId } } }); const result = db.discussion.findMany({ where: { teamId: { equals: user?.teamId } }, take: 10, skip: 10 * (page - 1), }); // 写入:请求体合并当前登录用户 const data = (await request.json()) as DiscussionBody; const created = db.discussion.create({ teamId: user?.teamId, authorId: user?.id, ...data, }); await persistDb('discussion'); return HttpResponse.json(created);

写操作后统一调用persistDb('modelName')落盘;认证辅助函数(requireAuth/requireAdmin/authenticate)与networkDelay(测试 200 ms、开发 300–1000 ms 随机)都放在 utils.ts。

新增一个模型的完整路径:在db.tsmodels里加字段 → 在data-generators.ts里加createXxx→ 在handlers/下新建一个文件并导出xxxHandlers,最后到handlers/index.ts中展开。

5. 启动 MSW(三种入口)

同一批handlers通过不同入口挂到三种运行环境。

5.1 浏览器开发模式(Vite dev server)

mocks/index.ts 提供enableMocking()

import { env } from '@/config/env'; export const enableMocking = async () => { if (env.ENABLE_API_MOCKING) { const { worker } = await import('./browser'); const { initializeDb } = await import('./db'); await initializeDb(); return worker.start(); } };
  • browser.ts 用setupWorker(...handlers)创建 service worker。
  • package.json"msw": { "workerDirectory": "public" },即 worker 文件放在apps/react-vite/public/mockServiceWorker.js(仓库中已存在)。
  • 需要设置VITE_APP_ENABLE_API_MOCKING=true(env.ts 会剥掉VITE_APP_前缀),且VITE_APP_API_URL必填。

5.2 Vitest 集成测试

setup-tests.ts 用setupServer(server.ts)在 Node 里起 MSW:

beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); afterAll(() => server.close()); beforeEach(() => { // 打桩 ResizeObserver / btoa / atob 后 initializeDb(); }); afterEach(() => { server.resetHandlers(); resetDb(); });
  • onUnhandledRequest: 'error':请求到未注册的路径会直接失败,便于发现漏写 handler。
  • 测试环境persistDb被跳过,数据只存活于内存dbresetDb()localStorage即可重置。

5.3 E2E mock server(Playwright)

mock-server.ts 用@mswjs/http-middleware把同一批 handler 挂到 Express:

app.use(cors({ origin: env.APP_URL, credentials: true })); app.use(express.json()); app.use(createMiddleware(...handlers)); initializeDb().then(() => { app.listen(env.APP_MOCK_API_PORT, () => { console.log(`Mock API server started at http://localhost:${env.APP_MOCK_API_PORT}`); }); });

package.json中:

"run-mock-server": "vite-node mock-server.ts | pino-pretty -c", "test-e2e": "pm2 start \"yarn run-mock-server\" --name server && yarn playwright test"

默认端口APP_MOCK_API_PORT=8080test-e2e会先用 pm2 起 mock server,再跑 Playwright。

6. 验证数据确实进入 mock API

6.1 冒烟:healthcheck

任何入口下GET ${API_URL}/healthcheck都会经过networkDelay并返回{ ok: true },是确认 MSW 已挂上的最小探针。

6.2 造一条 user 再回读

以浏览器开发模式为例:

  1. 打开http://localhost:3000,确认VITE_APP_ENABLE_API_MOCKING=true已生效。
  2. 发一次POST ${API_URL}/auth/register,body 使用createUser()的默认字段(firstNamelastNameemailpasswordteamName)。
  3. auth.ts 的 register handler 会先按 email 去重,再db.user.createpersistDb('user'),最后写Set-Cookie并返回{ user, jwt }
  4. DevTools → Application → Local Storage 查看msw-db,应能看到新增的user数组,且password是 utils.ts 中hash()处理后的字符串。
  5. 再请求GET ${API_URL}/auth/me(带 cookie),返回{ data: user },其中user已由sanitizeUser去掉passwordiat字段。

6.3 测试里生成并落库

Vitest 中可以直接使用生成器:

import { db } from '@/testing/mocks/db'; import { createUser } from '@/testing/data-generators'; const payload = createUser({ email: 'seed@example.com' }); db.user.create(payload); const found = db.user.findFirst({ where: { email: { equals: payload.email } } }); expect(found?.email).toBe('seed@example.com');

因为测试环境persistDb被跳过、resetDb()afterEach里清localStorage,这条数据不会影响下一条用例。

7. 限制与排查要点

  • VITE_APP_API_URL必填:env.ts 里API_URL: z.string(),缺失或非法时启动即抛Invalid env provided
  • ENABLE_API_MOCKING只接受字符串'true'/'false':其他取值会让 env 校验直接失败。
  • resetDb()只清window.localStorage:Node.js 环境(E2E mock server)使用的mocked-db.json不会被resetDb()清理,需要自行处理该文件以重置状态。
  • persistDb在测试环境跳过:这是 setup-tests.ts 能靠resetDb()做 per-test 隔离的前提。
  • onUnhandledRequest: 'error':Vitest 里请求未注册路径会直接失败,遇到 unhandled request 报错先补 handler 再跑。
  • networkDelay在测试环境固定 200 ms,开发环境 300–1000 ms 随机(utils.ts),测试等待时间因此稳定,开发体验更贴近真实网络抖动。

下一步

需要切到真实 API 时,把VITE_APP_ENABLE_API_MOCKING置为false并配置真实VITE_APP_API_URLenableMocking()就不会再启动 worker;模型与 handler 保留下来也可以直接作为后端 API 契约的参考。

【免费下载链接】bulletproof-react🛡️ ⚛️ A simple, scalable, and powerful architecture for building production ready React applications.项目地址: https://gitcode.com/GitHub_Trending/bu/bulletproof-react

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 7:18:45

deer-flow沙盒运行时:内存隔离与多语言执行原理

1. “deer-flow”到底是什么&#xff1a;一个被误读的沙盒运行时项目最近在技术社区里&#xff0c;“deer-flow”这个词突然频繁出现在各类讨论帖、GitHub issue 标题&#xff0c;甚至 Python 和 Node.js 的安装故障排查帖里。它既不是 PyPI 上的知名包&#xff0c;也不是 npm …

作者头像 李华
网站建设 2026/9/10 7:18:11

Spring Boot多租户实战:芋道源码的租户隔离链路解析

做了几年Java后端&#xff0c;参与过的项目里十个有八个都会碰多租户。有的用独立数据库&#xff0c;有的用独立Schema&#xff0c;有的像芋道源码&#xff08;ruoyi-vue-pro&#xff09;这样直接在共享表里用tenant_id做隔离。这三种方案各有各的取舍&#xff0c;但如果你是在…

作者头像 李华
网站建设 2026/9/10 7:15:51

Python LSTM时间序列预测实战:状态管理与滚动预测

简介&#xff1a;本资源是一套完整可用的基于LSTM神经网络的时间序列预测实战代码包&#xff0c;面向人工智能初学者、数据科学学习者及需要快速落地时序建模任务的工程师。项目覆盖从原始数据清洗、特征工程构建、LSTM模型搭建与训练&#xff0c;到最终预测结果可视化全流程&a…

作者头像 李华