关于 umbraco-msw-testing
面向 Umbraco 后台扩展开发者的测试技能,介绍使用 MSW 拦截 API 并返回模拟响应。可在不启动 Umbraco 实例的情况下测试错误、加载和边界状态。
主要功能
- 01拦截 Umbraco API 请求
- 02模拟错误与验证响应
- 03模拟延迟和加载状态
- 04提供 MSW v2 处理器模式
- 05集成 Web Test Runner 测试
适用场景
- 01验证 API 错误处理
- 02测试加载指示器和骨架屏
- 03检查网络重试行为
- 04无需后端测试边界情况
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ umbraco/ umbraco-cms-backoffice-skills --skill umbraco-msw-testing安装后在 Claude Code 中输入/umbraco-msw-testing确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
examples/api-mocking/mockServiceWorker.jspackage-lock.jsonpackage.jsonREADME.mdtsconfig.jsonweb-test-runner.config.mjsexamples/api-mocking/src/items-list.element.test.tsitems-list.element.tsexamples/api-mocking/src/mocks/handlers.tsitems.db.tssetup.ts详细介绍
Umbraco MSW 测试
这是什么?
MSW(Mock Service Worker)可拦截 API 调用并返回模拟响应,从而支持测试 Umbraco 后台扩展。无需运行 Umbraco 实例,即可轻松测试错误状态、加载状态和边缘情况。
适用场景
- 测试 API 错误处理(404、500、验证错误)
- 测试加载旋转图标和骨架屏状态
- 测试网络重试行为
- 无需设置后端即可测试边缘情况
- 在单元测试中添加 API 模拟
相关技能
- umbraco-testing - 测试概览的主技能
- umbraco-unit-testing - 单元测试模式(与 MSW 结合使用)
文档
- MSW 文档:https://mswjs.io/docs/
- 参考处理程序:
Umbraco-CMS/src/Umbraco.Web.UI.Client/src/mocks/handlers/
设置
依赖项
添加到 package.json:
{
"devDependencies": {
"@open-wc/testing": "^4.0.0",
"@web/dev-server-esbuild": "^1.0.0",
"@web/dev-server-import-maps": "^0.2.0",
"@web/test-runner": "^0.18.0",
"@web/test-runner-playwright": "^0.11.0",
"msw": "^2.7.0"
},
"scripts": {
"postinstall": "npx msw init . --save",
"test": "web-test-runner",
"test:watch": "web-test-runner --watch"
}
}
然后运行:
npm install
npx playwright install chromium
postinstall 脚本会将 mockServiceWorker.js 复制到项目根目录。没有此文件,MSW 将静默失败。
配置
创建 web-test-runner.config.mjs:
import { esbuildPlugin } from '@web/dev-server-esbuild';
import { playwrightLauncher } from '@web/test-runner-playwright';
import { importMapsPlugin } from '@web/dev-server-import-maps';
export default {
rootDir: '.',
files: ['./src/**/*.test.ts', '!**/node_modules/**'],
nodeResolve: {
exportConditions: ['development'],
preferBuiltins: false,
browser: false,
},
browsers: [playwrightLauncher({ product: 'chromium' })],
plugins: [
importMapsPlugin({
inject: {
importMap: {
imports: {
'@umbraco-cms/backoffice/external/lit': '/node_modules/lit/index.js',
'@umbraco-cms/backoffice/lit-element':
'/node_modules/@umbraco-cms/backoffice/dist-cms/packages/core/lit-element/index.js',
'@umbraco-cms/backoffice/element-api':
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/element-api/index.js',
'@umbraco-cms/backoffice/observable-api':
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/observable-api/index.js',
'@umbraco-cms/backoffice/context-api':
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/context-api/index.js',
'@umbraco-cms/backoffice/controller-api':
'/node_modules/@umbraco-cms/backoffice/dist-cms/libs/controller-api/index.js',
'@umbraco-cms/backoffice/class-api':
'/node_modules/@umbraco-cms/backoffice/dist-cms/packages/core/class-api/index.js',
},
},
},
}),
esbuildPlugin({
ts: true,
tsconfig: './tsconfig.json',
target: 'auto',
json: true,
}),
],
testRunnerHtml: (testFramework) =>
`<html lang="en-us">
<head>
<meta charset="UTF-8" />
<script src="/node_modules/msw/lib/iife/index.js"></script>
</head>
<body>
<script type="module" src="${testFramework}"></script>
</body>
</html>`,
};
目录结构
my-extension/
├── src/
│ ├── my-element.ts
│ ├── my-element.test.ts
│ └── mocks/
│ ├── handlers.ts # MSW handlers
│ ├── setup.ts # Worker setup
│ └── data/
│ └── items.db.ts # Mock database
├── mockServiceWorker.js # Generated by postinstall
├── web-test-runner.config.mjs
├── package.json
└── tsconfig.json
模式
MSW v2 语法
Umbraco 使用 MSW v2。主要 API 模式:
| 概念 | MSW v2 语法 |
|---|---|
| HTTP 方法 | http.get()、http.post()、http.put()、http.delete() |
| JSON 响应 | HttpResponse.json(data) |
| 状态码 | HttpResponse.json(data, { status: 201 }) |
| 空响应 | new HttpResponse(null, { status: 204 }) |
| 请求参数 | ({ params }) => { ... } |
| 请求正文 | ({ request }) => { const body = await request.json(); } |
| 延迟 | await delay(2000) |
全局 MSW 访问
const { http, HttpResponse, delay } = window.MockServiceWorker;
umbracoPath 辅助函数
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
// Creates: /umbraco/management/api/v1/document/:id
umbracoPath('/document/:id')
基本处理程序
GET 处理程序:
const { http, HttpResponse } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const handlers = [
http.get(umbracoPath('/document/:id'), ({ params }) => {
const id = params.id as string;
return HttpResponse.json({
id,
name: 'Test Document',
documentType: { alias: 'testType' },
});
}),
];
POST 处理程序:
http.post(umbracoPath('/document'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{
type: 'validation',
status: 400,
errors: { name: ['Name is required'] },
},
{ status: 400 }
);
}
const newId = crypto.randomUUID();
return HttpResponse.json(
{ id: newId },
{
status: 201,
headers: { 'Umb-Generated-Resource': newId },
}
);
}),
PUT 处理程序:
http.put(umbracoPath('/document/:id'), async ({ params, request }) => {
const id = params.id as string;
const body = await request.json();
mockDb.update(id, body);
return new HttpResponse(null, { status: 200 });
}),
DELETE 处理程序:
http.delete(umbracoPath('/document/:id'), ({ params }) => {
const id = params.id as string;
mockDb.delete(id);
return new HttpResponse(null, { status: 200 });
}),
模拟状态
错误响应:
// 404 Not Found
http.get(umbracoPath('/document/:id'), ({ params }) => {
const doc = mockDb.read(params.id as string);
if (!doc) return new HttpResponse(null, { status: 404 });
return HttpResponse.json(doc);
}),
// 500 Server Error
http.get(umbracoPath('/document/:id'), () => {
return HttpResponse.json(
{ type: 'error', detail: 'Internal server error' },
{ status: 500 }
);
}),
验证错误:
http.post(umbracoPath('/document'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{
type: 'validation',
errors: {
name: ['Name is required'],
title: ['Title must be at least 3 characters'],
},
},
{ status: 400 }
);
}
return new HttpResponse(null, { status: 201 });
}),
延迟响应(加载状态):
http.get(umbracoPath('/slow-endpoint'), async () => {
await delay(2000);
return HttpResponse.json({ data: 'loaded' });
}),
模拟数据库模式
// src/mocks/data/items.db.ts
interface Item {
id: string;
name: string;
value: number;
}
class ItemsMockDb {
private data: Item[] = [
{ id: '1', name: 'Item 1', value: 100 },
{ id: '2', name: 'Item 2', value: 200 },
];
read(id: string) {
return this.data.find((item) => item.id === id);
}
readAll() {
return [...this.data];
}
create(item: Omit<Item, 'id'>) {
const newItem = { ...item, id: crypto.randomUUID() };
this.data.push(newItem);
return newItem.id;
}
update(id: string, updates: Partial<Item>) {
const index = this.data.findIndex((i) => i.id === id);
if (index !== -1) {
this.data[index] = { ...this.data[index], ...updates };
}
}
delete(id: string) {
this.data = this.data.filter((i) => i.id !== id);
}
}
export const itemsDb = new ItemsMockDb();
Worker 设置
// src/mocks/setup.ts
const { setupWorker } = window.MockServiceWorker;
import { handlers } from './handlers.js';
const worker = setupWorker(...handlers);
export const startMockServiceWorker = () =>
worker.start({
onUnhandledRequest: 'warn',
quiet: true,
});
与测试集成
在测试文件中:
import { expect, fixture } from '@open-wc/testing';
import { startMockServiceWorker } from './mocks/setup.js';
import './my-element.js';
// Start MSW before tests
before(async () => {
await startMockServiceWorker();
});
describe('MyElement with API', () => {
it('displays data from API', async () => {
const element = await fixture(html`<my-element></my-element>`);
await element.updateComplete;
// Element should show mocked data
expect(element.shadowRoot?.textContent).to.include('Item 1');
});
});
示例
完整的处理程序文件
// src/mocks/handlers.ts
const { http, HttpResponse } = window.MockServiceWorker;
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
import { itemsDb } from './data/items.db.js';
export const handlers = [
// List items
http.get(umbracoPath('/my-extension/items'), () => {
const items = itemsDb.readAll();
return HttpResponse.json({ total: items.length, items });
}),
// Get single item
http.get(umbracoPath('/my-extension/items/:id'), ({ params }) => {
const item = itemsDb.read(params.id as string);
if (!item) return new HttpResponse(null, { status: 404 });
return HttpResponse.json(item);
}),
// Create item
http.post(umbracoPath('/my-extension/items'), async ({ request }) => {
const body = await request.json();
if (!body.name) {
return HttpResponse.json(
{ type: 'validation', errors: { name: ['Required'] } },
{ status: 400 }
);
}
const id = itemsDb.create(body);
return HttpResponse.json(
{ id },
{
status: 201,
headers: { 'Umb-Generated-Resource': id },
}
);
}),
// Update item
http.put(umbracoPath('/my-extension/items/:id'), async ({ params, request }) => {
const id = params.id as string;
if (!itemsDb.read(id)) return new HttpResponse(null, { status: 404 });
itemsDb.update(id, await request.json());
return new HttpResponse(null, { status: 200 });
}),
// Delete item
http.delete(umbracoPath('/my-extension/items/:id'), ({ params }) => {
const id = params.id as string;
if (!itemsDb.read(id)) return new HttpResponse(null, { status: 404 });
itemsDb.delete(id);
return new HttpResponse(null, { status: 200 });
}),
];
处理程序组织方式
src/mocks/
├── handlers.ts # Aggregates all handlers
├── setup.ts # Worker setup
├── handlers/
│ ├── document.handlers.ts
│ ├── media.handlers.ts
│ └── my-extension.handlers.ts
└── data/
├── document.db.ts
└── items.db.ts
// handlers.ts
import { documentHandlers } from './handlers/document.handlers.js';
import { mediaHandlers } from './handlers/media.handlers.js';
import { myExtensionHandlers } from './handlers/my-extension.handlers.js';
export const handlers = [
...documentHandlers,
...mediaHandlers,
...myExtensionHandlers,
];
运行测试
# Run all tests
npm test
# Run in watch mode
npm run test:watch
# Run specific file
npx web-test-runner src/my-element.test.ts
故障排除
MSW 未拦截请求
- 检查项目根目录中是否存在
mockServiceWorker.js - 确认测试 HTML 中已加载 MSW 脚本:
<script src="/node_modules/msw/lib/iife/index.js"></script> - 确保在运行测试前启动 worker
“http is not defined”
使用全局访问方式:const { http, HttpResponse } = window.MockServiceWorker;
处理程序不匹配
检查路径是否完全匹配。对于 Umbraco API 路径,请使用 umbracoPath()。
请求仍然发送到真实服务端
确保已设置 onUnhandledRequest: 'warn',以便在控制台中查看未处理的请求。
从 MSW v1 迁移
如果从 MSW v1 升级,以下是主要变化:
| MSW v1 | MSW v2 |
|---|---|
rest.get() | http.get() |
rest.post() | http.post() |
(req, res, ctx) => res(ctx.json(data)) | () => HttpResponse.json(data) |
res(ctx.status(404)) | new HttpResponse(null, { status: 404 }) |
res(ctx.delay(2000), ctx.json(data)) | await delay(2000); return HttpResponse.json(data) |
req.params.id | ({ params }) => params.id |
await req.json() | ({ request }) => await request.json() |
评论
支持 Markdown