🏐

PlaywrightでGraphQLのリクエストをモックしたい

2023/12/30に公開

GraphQLを用いたアプリケーションにe2e(Playwright)を導入することになり、苦戦するポイントがあったので記事にします。

問題

Playwrightでe2eテストを実施するにあたり、実際にAPIを叩くのではなく、リクエストをモックする方針になりました。(リクエストをモックしてしまっては、e2e(end-to-end)と呼べるのだろうかという問題もありますが、本記事ではe2eテストと呼称することとします。)
サービスはAPIをGraphQLで構築しており、APIのエンドポイントは1つです。
https://**/graphql
Playwrightはビルトインでエンドポイント毎にリクエストをモックするメソッドを提供しています。
たとえばページ遷移のタイミング等でリクエストが1種類のみ走る場合はこの方法で良いのですが、複数種類走る場合は、少し工夫が必要です。

解決策

下記の記事を参考にリクエストをモックする処理を実装しました。
https://www.jayfreestone.com/writing/stubbing-graphql-playwright/

前提

今回モックする対象は下記状況を想定しています。
(フロントエンドエンジニアなりたての時に試しに作ってみたTODOアプリを引っ張ってきました。UIがイケてないのは多めにみてください笑)

このページに遷移したときに、2つのリクエストが走っています。urlは${baseURL}/listです。

  • getUser
    • ログインユーザー情報を取得する
{
    "data": {
        "user": {
            "id": 1,
            "email": "test@example.com",
            "__typename": "UserModel"
        }
    }
}
  • getAllTasks
    • TODO Listを取得する
{
    "data": {
        "task": [
            {
                "id": 2,
                "title": "TEST Tilte",
                "status": {
                    "id": 1,
                    "name": "todo",
                    "__typename": "StatusModel"
                },
                "statusId": 1,
                "date": "2023/12/20",
                "memo": "hoge fuga...",
                "__typename": "TaskModel"
            },
	    // ...省略
        ]
    }
}

普通にモックしてみる

ビルトインのAPIモック方法でリクエストをモックしてみます。

e2e/list/index.test.ts
import { test, expect } from '@playwright/test';

test('テスト', async ({ page }) => {
  await page.goto('http://localhost:3000/list');

  // getUserと getAllTasksの2つのAPIが走るので2回モック
  await page.route('http://localhost:8000/graphql', async (route) => {
    const json = {
      data: {
        user: {
          id: 2,
          email: 'test2@gmail.com',
          __typename: 'UserModel'
        }
      }
    };

    await route.fulfill({ json });
  });

  await page.route('http://localhost:8000/graphql', async (route) => {
    const json = {
      data: {
        task: [
          {
            id: 2,
            title: 'TEST Tilte',
            status: {
              id: 1,
              name: 'todo',
              __typename: 'StatusModel'
            },
            statusId: 1,
            date: '2023/12/20',
            memo: 'hoge fuga...',
            __typename: 'TaskModel'
          },
	//...省略
        ]
      }
    };

    await route.fulfill({ json });
  });

  const card = page.getByTestId('playwrightTest').first();

  await expect(card).toBeVisible();
});

上記のテストをデバッグモードで実行してみましょう

npx playwright test example.test.ts --headed --debug

検証ツールで確認してみると2つのリクエストをモックできているようですが、よくみると2つともレスポンスがtaskになっていました(getUserのレスポンスがgetAllTasksになっていた)。つまり一番最後のモックが全てのリクエストのレスポンスを上書きしてしまうようです。

それぞれのリクエストをモックする方法

冒頭で紹介したこちらの記事を参考に修正していきます。

e2e/interceptGQL.ts
import { Page, Route } from '@playwright/test';

export async function interceptGQL(page: Page): Promise<void> {
  // GraphQLエンドポイントへのリクエストをインターセプトする
  await page.route('http://localhost:8000/graphql', function (route: Route) {
    const req = route.request().postDataJSON();

    const operationName = req.operationName;

    let resp;
    switch (operationName) {
      case 'getUser':
        resp = {
          data: {
            user: {
              id: 1,
              email: 'test@example.com',
              __typename: 'UserModel'
            }
          }
        };
        break;
      case 'getAllTasks':
        resp = {
          data: {
            task: [
              {
                id: 2,
                title: 'TEST Tilte',
                status: {
                  id: 1,
                  name: 'todo',
                  __typename: 'StatusModel'
                },
                statusId: 1,
                date: '2023/12/20',
                memo: 'hoge fuga...',
                __typename: 'TaskModel'
              }
              // ...省略
            ]
          }
        };
        break;
      default:
        resp = null;
    }

    if (resp === null) {
      return route.fallback();
    }

    return route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify(resp)
    });
  });
}

interceptGQLという関数を定義します。その中でGraphQLエンドポイントへのリクエストをインターセプトします。GraqhQLの場合、エンドポイントが1つしかなくても、operationName(クエリ名)が異なるので、この値によってモックするレスポンスを出し分けます。

次に、Playwrightのtest関数をinterceptGQLが使えるように拡張します。
(参考:https://playwright.dev/docs/api/class-test#test-extend)

e2e/testUtils.ts
import { test as baseTest } from '@playwright/test';

import { interceptGQL } from './interceptGQL';

export const test = baseTest.extend<{ interceptGQL: typeof interceptGQL }>({
  interceptGQL: async ({}, use) => {
    await use(interceptGQL);
  }
});

最後に対象のtestファイルで使うtestをtestUtilsから読み込んでGraphQLのリクエストが走るタイミングでinterceptGQLを呼び出します。

e2e/list/index.test.ts
import { expect } from '@playwright/test';

import { test } from '../testUtils';

test('テスト', async ({ page, interceptGQL }) => {
  await page.goto('http://localhost:3000/list');
  
  // ページ遷移のタイミングでリクエストが走るのでここで呼び出し
  await interceptGQL(page);

  const card = page.getByTestId('playwrightTest').first();

  await expect(
    page.getByRole('heading', { name: 'dashboard', level: 3 })
  ).toBeVisible();
  
  // ...省略
});

もう一度、デバッグモードで実行してみましょう。
今度はgetUser, getAllTasksどちらのクエリも正しいレスポンスが返ってきているのが確認できました。

Discussion