mcpField
U
umbraco-msw-testing

umbraco-msw-testing

github.com/umbracoテスト・品質27371 回インストール6 週間前に更新
リポジトリ

umbraco-msw-testing について

Umbraco のバックオフィス拡張を開発する人向けのテストスキルです。MSW で API 呼び出しを捕捉してモック応答を返し、Umbraco を起動せずにエラーや読み込み、境界状態をテストできます。

主な機能

  1. 01Umbraco API リクエストの捕捉
  2. 02エラーと検証応答のモック
  3. 03遅延と読み込み状態の再現
  4. 04MSW v2 ハンドラーパターン
  5. 05Web Test Runner との連携

活用シーン

  1. 01API エラー処理の検証
  2. 02スピナーやスケルトンのテスト
  3. 03ネットワークリトライ動作の確認
  4. 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.js
package-lock.json
package.json
README.md
tsconfig.json
web-test-runner.config.mjs
examples/api-mocking/src/
items-list.element.test.ts
items-list.element.ts
examples/api-mocking/src/mocks/
handlers.ts
items.db.ts
setup.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 がリクエストをインターセプトしない
  1. mockServiceWorker.js がプロジェクトルートに存在することを確認する
  2. テスト HTML で MSW スクリプトが読み込まれていることを確認する: <script src="/node_modules/msw/lib/iife/index.js"></script>
  3. テストが実行される前に worker が起動されていることを確認する
「http is not defined」

グローバルアクセスを使用します: const { http, HttpResponse } = window.MockServiceWorker;

ハンドラーが一致しない

パスが完全に一致していることを確認してください。Umbraco API パスには umbracoPath() を使用します。

リクエストが依然として実際のサーバーに到達する

コンソールで未処理のリクエストを確認するには、onUnhandledRequest: 'warn' が設定されていることを確認してください。


MSW v1 からの移行

MSW v1 からアップグレードする場合、主な変更点は次のとおりです:

MSW v1MSW 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()

コメント