👹

Playwrightで同一テスト内で複数アカウントを使用する方法

2024/10/08に公開

実務にあるような管理者や一般ユーザのようなアカウントを、Playwrightのテストケース内で即座に切り替える実践的な方法をご紹介します。

📔 結論

公式にある Testing multiple roles with POM fixtures の方法を使用します。

例えば実務でありそうな以下のテストケースを想定してみます。

テストケース例

前提 期待値
管理者でログインし、ボード一覧を表示する ボードを作成 ボタンが表示されている
管理者でログインし、ボード一覧を表示する ボードがありません。新しいボードを作りましょう が表示されている
一般ユーザでログインし、ボード一覧を表示する ボードを作成 ボタンが表示されない
一般ユーザでログインし、ボード一覧を表示する ボードがありません。 が表示されている

これを Testing multiple roles with POM fixturesを用いて実装すると、以下のように1テストケース内で即座にアカウントを切り替えることができ、とても便利になります。

tests/boards.test.ts
import { test, expect } from '@/src/playwright'

test.describe('XXX画面', async () => {
  test('初期表示: ボードが0件', async ({ admin, user }) => {
    // 管理者はボードを作成することができる
    await expect(admin.page.getByRole('button', { name: 'ボードを作成' })).toBeVisible();
    await expect(admin.page.getByRole('button', { name: 'ボードを作成' })).toBeEnabled();
    await expect(admin.page.getByText(/ボードがありません。新しいボードを作りましょう/)).toBeVisible();

    // 一般ユーザはボードを作成することができないし表示されない
    await expect(user.page.getByRole('button', { name: 'ボードを作成' })).not.toBeVisible();
    await expect(user.page.getByText(/ボードがありません。/)).toBeVisible();
  });
});

ここで疑問が湧くと思います。
adminuserはいつどこでどのように生成されているのか...と。
最もフェティッシュなやり方で説明したいと思います。

🧠 Testing multiple roles with POM fixtures の仕組み

実は1行目のimport文は@playwright/testから提供されているものを使用していません。

tests/boards.test.ts
import { test, expect } from "@/src/playwright";

なぜなら、@playwright/testから提供されたものを使用すると、adminuserといったアカウントを利用することができず、以下の形式を使うことしかできないからです。

tests/boards.test.ts
test("初期表示: ボードが0件", async ({ page }) => {
  // 🥲 ログインしていない判定となりテストがNGになるよ
  await expect(page.getByRole("button", { name: "ボードを作成" })).toBeVisible();
});

そのため、私が@playwright/testを拡張して src/playwright/index.tsにファイルを配置しているんですね。
その理由がズバリ、Testing multiple roles with POM fixtures の仕組みを利用するため、ということになります。

つまり、@playwright/testから提供されるtest関数を拡張して、第一引数にadminuserなどの権限ごとのログイン済みアカウント達を引き渡すためのファイルという訳です。

コードの全容はこちらです。

src/playwright/index.ts
import { test as base, type Page } from "@playwright/test";

class AdminPage {
  page: Page;
  constructor(page: Page) {
    this.page = page;
  }
}

class UserPage {
  page: Page;
  constructor(page: Page) {
    this.page = page;
  }
}

type AuthorizedFixtures = {
  admin: AdminPage;
  user: UserPage;
};

const signIn = async (page: Page, email: string, password: string): Promise<void> => {
  await page.goto("https://hoge.com/signin"); // 環境変数使うべき
  await page.waitForURL("https://hoge.com/signin"); // 環境変数使うべき
  await page.getByLabel("メールアドレス").fill(email);
  await page.getByLabel("パスワード").fill(password);
  await page.getByRole("button", { name: "ログイン" }).click();
}

export * from "@playwright/test";
export const test = base.extend<AuthorizedFixtures>({
  admin: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: "src/playwright/.auth/admin.json",
    });
    const adminPage = new AdminPage(await context.newPage());
    await signIn(adminPage.page, "admin@hoge.com", "IamFetishMan");
    await use(adminPage);
    await context.close();
  },
  user: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: "src/playwright/.auth/user.json",
    });
    const userPage = new UserPage(await context.newPage());
    await signIn(userPage.page, "admin@hoge.com", "IamFetishMan");
    await use(userPage);
    await context.close();
  },
});

まずは、import文の test as baseによってbaseという別名がつけられていますが、これは後に解決するtestの関数名でexportしたいため、名前の競合を起さないために別名をつけています。

import { test as base, type Page } from "@playwright/test";

以下はPOM(Page Object Model)という繰り返しを避けるアプローチの一つです。
今回は大したことはしないためテンプレのまま使います。

class AdminPage {
  page: Page;
  constructor(page: Page) {
    this.page = page;
  }
}

class UserPage {
  page: Page;
  constructor(page: Page) {
    this.page = page;
  }
}

test関数の第一引数に渡すための型を定義してあげます。名前はなんでもOKです。

type AuthorizedFixtures = {
  admin: AdminPage;
  user: UserPage;
};

どのアカウントだとしてもログインするまでの動線は同じなので共通に使用する関数を作成しました。
CIに載せる場合がほとんどだと思うので、現場では環境変数を使いましょう。

const signIn = async (page: Page, email: string, password: string): Promise<void> => {
  await page.goto("https://hoge.com/signin"); // 環境変数使うべき
  await page.waitForURL("https://hoge.com/signin"); // 環境変数使うべき
  await page.getByLabel("メールアドレス").fill(email);
  await page.getByLabel("パスワード").fill(password);
  await page.getByRole("button", { name: "ログイン" }).click();
}

重要な部分ですので詳しく説明していきます。

export * from "@playwright/test";
export const test = base.extend<AuthorizedFixtures>({
  admin: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: "src/playwright/.auth/admin.json",
    });
    const adminPage = new AdminPage(await context.newPage());
    await signIn(adminPage.page, "admin@hoge.com", "IamFetishMan");
    await use(adminPage);
    await context.close();
  },
  user: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: "src/playwright/.auth/user.json",
    });
    const userPage = new UserPage(await context.newPage());
    await signIn(userPage.page, "admin@hoge.com", "IamFetishMan");
    await use(userPage);
    await context.close();
  },
});

まず、利用側でtest関数を呼ぶと、"@playwright/test"でexportされているtestではなく、次行の export const testの方が使われます。

理由はbase.extendsによって、testの第一引数に渡すフィクスチャを変更したいからですね。
フィクスチャは AuthorizedFixtures型を使用します。

export * from "@playwright/test";
export const test = base.extend<AuthorizedFixtures>({

AuthorizedFixtures型に対応するadminuserキーを指定しています。
そして、

{
  admin: async ({ browser }, use) => {
    // 〜処理〜
  },
  user: async ({ browser }, use) => {
    // 〜処理〜
  },
}

テストごとに独立したブラウザを立ち上げるためにbrowser.newContextを使用します。
また、storageState: "src/playwright/.auth/admin.json"認証の状態をファイルストレージに保存する可能性があるため一応作っておきます(中身は{}でOK)

const context = await browser.newContext({
  storageState: "src/playwright/.auth/admin.json",
});

AdminPageというPOMをインスタンス化し、ログイン処理を実行します。
useにて、adminPageを呼び出す全testファイルのテストプロセスが実行され、最終的にブラウザコンテキストを閉じる、というのが Testing multiple roles with POM fixturesの全容となります。

const adminPage = new AdminPage(await context.newPage());
await signIn(adminPage.page, "admin@hoge.com", "IamFetishMan");
await use(adminPage);
await context.close();

まとめ

全然記事を書いていなかったので、見づらいことこの上ありませんがいかがでしたか?

実務や現場で複数アカウントを切り替えてテストすることは、当たり前のようで手動でやると面倒くさい要素の一つですよね。
今回の記事がそんな方に助けになれれば感謝感激120点満点です。

読んでいただきありがとうございました。ではまた来週 🤝

Discussion