はじめに
どのテストでも同じ前提条件(ログイン済み、DB接続など)を毎回用意する必要があり、各テストにベタ書きすると重複や書き忘れが増えます。これをテスト本体から追い出し、テストを書きやすくするのがこの記事の目的です。
その手段が fixture です。前提条件を準備し、テストに渡し、後片付けする仕組みで、Playwright 固有ではなく pytest や JUnit などにも共通する一般的なテスト用語です。
Playwright で前提条件を整える方法は複数あります(beforeEach / afterEach、グローバルセットアップなど)。中でも base.extend は既存の test を拡張して fixture を追加・上書きする API で、次の点で優れています。
-
オンデマンド実行:要求した fixture だけが準備される(
beforeEachは全テストで無条件に走る) -
組み込み fixture の上書き:
pageなど注入する値そのものに前処理を埋め込める use()による自動後片付け- 型安全:追加した fixture が引数として型補完される
本記事では自動ログインを例に、「関数」と「base.extend による fixture」を比較します。
問題
各テストの開始時にUIログインし、終了時にログアウトする状況を考えます。
まず、ログイン画面の操作を1クラスにまとめた LoginPage を用意します(Page Object Model:画面ごとの操作をクラスに部品化する設計手法)。
import { Page } from "@playwright/test";
export class LoginPage {
constructor(private readonly page: Page) {}
async goto(path: string) {
await this.page.goto(path);
}
async login(userId: string, password: string) {
await this.page.getByLabel("ユーザーID").fill(userId);
await this.page.getByLabel("パスワード").fill(password);
await this.page.getByRole("button", { name: "ログイン" }).click();
}
async logout() {
await this.page.getByRole("button", { name: "ログアウト" }).click();
}
}
これを素直に関数で書くと、こうなります。
async function autoLogin(page) {
const login = new LoginPage(page);
await login.goto(LOGIN_PATH);
await login.login(userId, password);
}
test("A", async ({ page }) => {
await autoLogin(page); // ← 各テストで毎回呼ぶ
...
});
このやり方には、2つの弱点があります。
弱点1:各テストが呼び出しの責任を負う
autoLogin(page) を呼ぶかは各テスト任せで、書き忘れると未ログインのまま実行されます。エラーにならず「なぜか失敗する」状態になり、原因の特定に時間がかかります。
test("B", async ({ page }) => {
// autoLogin の呼び忘れ → 未ログインで実行される
...
});
弱点2:後片付けを毎回手書きする必要がある
テスト後にログアウトしたい場合、関数では後片付けを毎テストに try/finally で書くことになります。
test("A", async ({ page }) => {
const login = new LoginPage(page);
await login.goto(LOGIN_PATH);
await login.login(userId, password);
try {
...
} finally {
await login.logout(); // 毎回手書き
}
});
解決方法
これらを base.extend による fixture で解決します。
解決1:page を上書きしてログインを埋め込む
base.extend では既存の page を上書きできます。注入自体にログイン処理を埋め込めば、page を受け取った時点で必ずログイン済みになり、呼び忘れが起こりません。
export const test = base.extend<Pages & AuthOptions>({
autoLogin: [true, { option: true }],
// 左の page: が「これから上書きする page fixture」
// 引数の { page } は「Playwright標準の page(上書き前の元)」
page: async ({ page, autoLogin }, use) => {
if (autoLogin) {
const loginPage = new LoginPage(page);
await loginPage.goto(LOGIN_PATH);
await loginPage.login(userId, password);
}
await use(page);
},
});
紛らわしいのが2つの page です。キー側の page: は上書きする fixture の名前、引数の { page } はPlaywright標準の元の page です。上書きは同名で宣言する仕様のため一致します。元の page にログインを済ませ、use() でテストに渡します。
テスト側で使う homePage も fixture として定義しておきます。インスタンス生成を fixture に任せれば、テスト側は new HomePage(page) を書かずに homePage を引数で受け取れます。
export const test = base.extend<Pages & AuthOptions>({
autoLogin: [true, { option: true }],
page: async ({ page, autoLogin }, use) => {
if (autoLogin) {
const loginPage = new LoginPage(page);
await loginPage.goto(LOGIN_PATH);
await loginPage.login(userId, password);
}
await use(page);
},
// HomePage のインスタンスを生成してテストに渡す fixture
homePage: async ({ page }, use) => {
await use(new HomePage(page));
},
});
これでテスト本体は、ログイン済み・生成済みの homePage を受け取り、検証したい操作だけに集中できます。
test("キーワードで検索するとヒット件数が表示される", async ({ homePage }) => {
// autoLogin により、この行に来た時点で既にログイン済み
await homePage.search("playwright");
await expect(homePage.resultCount).toHaveText("3件");
});
解決2:use() で後片付け(ログアウト)を自動化する
後片付けは use() を境界にして書きます。await use(value) はそこで停止して value をテストに渡し、テストが終わると呼び出しが返って続きの行(後片付け)が走ります。先ほどの page 上書きにログアウトを足すとこうなります。
page: async ({ page, autoLogin }, use) => {
const loginPage = new LoginPage(page);
if (autoLogin) {
await loginPage.goto(LOGIN_PATH);
await loginPage.login(userId, password); // use前:ログイン
}
await use(page); // ここでテストが走る
if (autoLogin) {
await loginPage.logout(); // use後:ログアウト
}
},
return page にしないのは、return すると後片付けを書く場所がなくなるからです。use() は処理をテストに明け渡す一時停止点で、完了後に制御が戻ります。しかも成功・失敗どちらでも後片付けが走る(内部が try/finally 相当)ため、自分で try/finally を書く必要がありません。Reactの useEffect のクリーンアップや pytest の yield と同じ発想です。
おわりに
関数でも実装できますが、base.extend による fixture は次の点で優れています。
-
pageの上書きで全テストが必ずログイン済みになり、呼び忘れが起こらない -
use()を境界に、成功・失敗によらず後片付けが自動実行される
全テストで前提を確実に揃えたい場合は fixture が有力です。一部でしか使わない単発のヘルパーなら普通の関数で十分なので、用途に応じて使い分けましょう。
参考
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてください!
▼▼▼
https://projisou.jp