Playwrightで同一テスト内で複数アカウントを使用する方法
実務にあるような管理者や一般ユーザのようなアカウントを、Playwrightのテストケース内で即座に切り替える実践的な方法をご紹介します。
📔 結論
公式にある Testing multiple roles with POM fixtures の方法を使用します。
例えば実務でありそうな以下のテストケースを想定してみます。
テストケース例
前提 | 期待値 |
---|---|
管理者でログインし、ボード一覧を表示する |
ボードを作成 ボタンが表示されている |
管理者でログインし、ボード一覧を表示する |
ボードがありません。新しいボードを作りましょう が表示されている |
一般ユーザでログインし、ボード一覧を表示する |
ボードを作成 ボタンが表示されない |
一般ユーザでログインし、ボード一覧を表示する |
ボードがありません。 が表示されている |
これを Testing multiple roles with POM fixtures
を用いて実装すると、以下のように1テストケース内で即座にアカウントを切り替えることができ、とても便利になります。
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();
});
});
ここで疑問が湧くと思います。
admin
やuser
はいつどこでどのように生成されているのか...と。
最もフェティッシュなやり方で説明したいと思います。
🧠 Testing multiple roles with POM fixtures の仕組み
実は1行目のimport文は@playwright/test
から提供されているものを使用していません。
import { test, expect } from "@/src/playwright";
なぜなら、@playwright/test
から提供されたものを使用すると、admin
やuser
といったアカウントを利用することができず、以下の形式を使うことしかできないからです。
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
関数を拡張して、第一引数にadmin
やuser
などの権限ごとのログイン済みアカウント達を引き渡すためのファイルという訳です。
コードの全容はこちらです。
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
型に対応するadmin
とuser
キーを指定しています。
そして、
{
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