Playwrightを用いたE2Eテストの自動化スクリプトを書く際に気をつけたこと

こんにちは、最近Playwrightを使ってE2Eテストの自動化をやっている者です。
近年、Playwrightの注目度は右肩上がりで伸びていて、Seleniumの人気に追いつかんばかりですね。
![スクリーンショット 2024-10-30 14.41.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2503518/882d08fb-15da-1d07-3319-b8e75454de8a.png)
[画像：GoogleTrends](https://trends.google.co.jp/trends/explore?date=today%205-y&geo=JP&q=Playwright,Selenium,Puppeteer,Cypress&hl=ja)より

今回は、壊れにくいE2Eテストのコードを書くために気をつけたことを紹介していきます。
また取り扱う内容はテストの実行時間を短くする上でも重要ですので、ぜひ取り入れてみてください。

# タグの指定にデベロッパモードで取得したXPathを使わない
例えばこんなHTMLがあるとします。名前の`input`タグをセレクトしたい時、あなたならどうしますか？
```html
<html>
<body>
    <form>
        <div>
            <label for="username">名前</label>
            <input type="text">
        </div>
        <div>
            <label for="memberId">社員番号</label>
            <input type="text">
        </div>
    </form>
</body>
</html>
```

普段、特に気にせずタグを指定するときには、XPathを使ってこのように書いています。
```javascript
const input = await page.locator('xpath=/html/body/form/div[1]/input')
```
ブラウザのデベロッパーツールを使うと要素のXPathを簡単に取得できるので、このように書くことが多いです。
しかしながら、今回は壊れにくいテストを作ることが目標です。このようにXPathを指定するとページに変更があった際に、locatorで失敗することが容易に想像できます。

ですので、私はこのように書きました。
```javascript
const input = await page
                .locator('label:text("名前")')
                .locator("..")
                .locator("input");
```

名前というテキストのあるラベルの兄弟関係の`input`タグをセレクトしています。
これで少しは壊れにくくなったかなと思います。

# テストが並列で実行されても問題ないように、各テストを独立させる
テストは並列で実行されてもいいように各テストが独立して動作するようになっている必要があります。
例えば、組織一覧画面で組織の登録・削除を行うテストがあるとします。
最初は、組織を3個作成して、それを消すのに削除ボタンを3回押すという簡単な実装にしていました。
```javascript
for (let i = 1; i < 4; i++) {
    await page.getByText("削除").first().click();
    await page.getByRole("button", { name: "確定" }).click();
}
```
しかし、テストが増えるにつれ、他のテストでも組織の作成が行われるようになりました。
そうすると、他のテストと並列で実行されたときに、誤って組織が削除されたり、完全に削除しきれないことがありました。

そのため、削除する時には組織名を指定して削除するボタンをクリックするようにしました。
```javascript
const divisions = ["事業部A", "事業部B", "事業部C"];
for (const division of divisions) {
    await page.getByText(division)
              .locator("..")
              .getByText("削除").click();

    await page.getByRole("button", { name: "確定" }).click();
}
```

他のテストでも同様に、あるテストが他のテストの影響を受けないように、検索結果にフィルターをかけて絞り込みを行なったり、テスト間で同じ組織の使い回しをしないようにするなどを心がけました。

# 不用意に`waitForTimeout`を使わない
テストの実行時間が伸びる理由に、無駄に`waitForTimeout`を使っているというのがあげられます。なんとなく、マシンの読み込み時間を設けたほうがいいような気がして、適当な時間の`waitForTimeout`を設定してしまうのは、よくあります。
```javascript
//こちらではなく
await page.waitForTimeout(300);
//こっちを使う
await page.waitForSelector("特定の要素セレクタ");
```
`waitForTimeout`ではなく`waitForSelector`を使うことで、セレクトした要素が表示されるまで待機するので、待つ時間を最適化することができます。

# タイムアウトを設定する

クリックする要素が見つからなかったときなど、テストが失敗するまでの時間はデフォルトで30秒かかります。そんなに長い間待っていられないので、`setDefaultTimeout`を5秒に設定しています。`setDefaultTimeout`は、そのコンテキスト内でのすべての操作に対して、デフォルトのタイムアウトを設定するものです。また、時間がかかるテストは`setTimeout`で最大実行時間を設定しています。`test.setTimeout`はテスト全体が完了するまでのタイムアウトを設定するものです。
```javascript
test.beforeEach(async ({ context }) => {
    context.setDefaultTimeout(5000);
});

test("時間がかかるテスト", async ({ page }) => {
    test.setTimeout(240000); // 240秒に設定
    //時間がかかる処理
});
```

また、コンフィグで設定するのも一つの方法です。
```typescript:playwright.config.ts
import { defineConfig } from '@playwright/test';  
 
export default defineConfig({  
 timeout: 4 * 60 * 1000,
});
```

# まとめ

以上をまとめると私が気をつけたのは以下の４つになります。
- タグの指定に安易にXPathを使わない
- 各テストが独立しているようにすること
- waitForTimeoutを多用しないこと
- デフォルトのタイムアウトとテスト全体が完了するまでのタイムアウトを設定すること

より良い方法があるという方はぜひコメントしてください。