PlaywrightでWebページやAPIのモックを作成する方法

Playwrightのモック機能

Playwrightはブラウザ自動操作だけでなく、API通信をモック(偽装)する機能も備えています。

これにより、開発中やバックエンドが未実装の段階でもフロントエンドのテストやUI開発を進めることが可能になります。

Webページのモックの作成方法

https://iwb.jp/hello/ にアクセスするとWebページが存在しないため、Statusが404になります。

Playwrightのテストの際にまだ作成されていないWebページ(リンク切れ)があって404になるとテストなどに支障が生じることがあります。

そんなときは、以下のようにpage.routeでURLを指定して、route.fulfillでstatus, contentType, bodyを返せばWebページのモックを作成してテストを通すことができます。

example.spec.ts
test('Helloページでh1タグを確認', async ({ page }) => {
  await page.route('https://iwb.jp/hello/', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'text/html',
      body: '<!DOCTYPE html><html><head><title>Hello World</title></head><body><h1>Hello</h1><div id="content">sample page</div></body></html>'
    })
  })

  await page.goto('https://iwb.jp/hello/')
  await expect(page.locator('h1')).toHaveText('Hello')
  await expect(page).toHaveTitle('Hello World')
})

URLがdev.iwb.jpのように複数ある場合は、ワイルドカード(**)を使用して、page.route('**/hello/' のように書くこともできます。

APIレスポンスのモックの作成方法

APIレスポンスのモックもpage.routeでURLを指定して、route.fulfillで作成できます。

contentTypeは 'text/html' ではなく 'application/json' になります。

example.spec.ts

test('ダミーのAPIレスポンスを返す', async ({ page }) => {
  await page.route('https://iwb.jp/api/products', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify([
        { id: 1, name: 'ダミー商品A', price: 1000 },
        { id: 2, name: 'ダミー商品B', price: 2000 }
      ])
    })
  })

  const response = await page.goto('https://iwb.jp/api/products')
  expect(response?.status()).toBe(200)
  const contentType = response?.headers()['content-type']
  expect(contentType).toContain('application/json')
  const responseText = await response?.text()
  expect(responseText).toBeDefined()
  const jsonData = JSON.parse(responseText || '')
  expect(Array.isArray(jsonData)).toBe(true)
  expect(jsonData.length).toBe(2)

  // 最初の商品のデータを確認
  expect(jsonData[0]).toHaveProperty('id', 1)
  expect(jsonData[0]).toHaveProperty('name', 'ダミー商品A')
  expect(jsonData[0]).toHaveProperty('price', 1000)

エラーにしたい場合は「status: 500」を指定します。

example.spec.ts
test('ダミーのAPIレスポンスを返す', async ({ page }) => {
  await page.route('https://iwb.jp/api/products', async route => {
    await route.fulfill({
      status: 500,
      contentType: 'application/json',
      body: '500エラーです。'
    })
  })

  const response = await page.goto('https://iwb.jp/api/products')
  expect(response?.status()).toBe(500)
  const contentType = response?.headers()['content-type']
  expect(contentType).toContain('application/json')
  const responseText = await response?.text()
  expect(responseText).toBe('500エラーです。')
})

harファイルを記録・読み込んでテストする

Webページで読み込んだ特定のAPIのレスポンスをharファイルに保存して、モックに利用する方法もあります。

例えば、以下のコードはrecordHarでharファイルのpathと読み込むurlFilterを指定しています。

Webページ内のボタンを押下したときに実行されるJSONを読み込み時のレスポンスをharファイルに記録して、routeFromHARでharファイルを読み込んでいます。

example.spec.ts
import { test, expect } from '@playwright/test';

test.describe('APIモックテスト', () => {
  test('HARファイルを使用したAPIモックテスト', async ({ browser }) => {
    // 記録用のコンテキストを作成
    const recordContext = await browser.newContext({
      recordHar: {
        path: './tests/api.har',
        urlFilter: '**/products/**'
      }
    });
    const recordPage = await recordContext.newPage();

    // ページにアクセス
    await recordPage.goto('https://iwb.jp/s/javascript-fetch-delay-timeout-abort-controller/');

    // ボタンクリック前にレスポンスを待機する準備
    const responsePromise = recordPage.waitForResponse(response =>
      response.url().includes('/products/') && response.status() === 200
    );

    // ボタンをクリック
    await recordPage.getByRole('button', { name: 'getProductJSON()' }).click();

    // APIレスポンスを待機
    const response = await responsePromise;

    // レスポンスの内容を確認
    const responseText = await response.text();
    console.log('Recorded response:', responseText);

    await recordContext.close();

    // 記録されたHARファイルを使用してモックテスト
    const mockContext = await browser.newContext();
    await mockContext.routeFromHAR('./tests/api.har', {
      url: '**/products/**',
      update: false
    });
    const mockPage = await mockContext.newPage();

    // 同じページにアクセス
    await mockPage.goto('https://iwb.jp/s/javascript-fetch-delay-timeout-abort-controller/');

    // モックされたAPIレスポンスでボタンをクリック
    const mockResponsePromise = mockPage.waitForResponse(response =>
      response.url().includes('/products/') && response.status() === 200
    );

    await mockPage.getByRole('button', { name: 'getProductJSON()' }).click();
    const mockResponse = await mockResponsePromise;
    const mockResponseText = await mockResponse.text();

    // モックレスポンスの内容を確認
    console.log('Mocked response:', mockResponseText);

    // レスポンスがJSON形式であることを確認
    expect(mockResponse.headers()['content-type']).toContain('application/json');

    // JSONデータが存在することを確認
    const jsonData = JSON.parse(mockResponseText);
    expect(jsonData).toBeDefined();
    expect(jsonData.id).toBe(1);
    console.log(jsonData.id)
    await mockContext.close();
  });
});

このコードはidが「1」になるかテストしていますが、harファイルの読み込みの処理部分をコメントアウトして、harファイル内のidを「2」にするとテストは失敗になります。

JSONのデータが膨大な場合はharファイルに記録して利用するとモックが作成しやすくなります。