0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

playwrightで共通処理(事前処理・後片付け)を書きやすくする方法

0
Posted at

はじめに

どのテストでも同じ前提条件(ログイン済み、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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?