Playwrightで始めるE2E自動テスト入門


この記事はDeNA23新卒Advent Calendar 2022の13日目の記事です。

https://qiita.com/advent-calendar/2022/dena-23-shinsotsu

## はじめに
皆さんはE2Eテストをどのように行っていますか？
中には手動で行っている人もいるかもしれませんが、playwrightを使うと手軽に自動でE2Eテストを行うことができます。
先日、インターン先の会社の開発で現在手動で行っているE2Eテストをplaywrightを用いて自動化させるといったタスクを行ったため、一通りplaywrightを触ることができました。
この記事では個人的な備忘録を兼ねて、Playwrightでできることを紹介し、実装する際の手順とコツについて考えていきたいと思います。

この記事の対象者
- E2Eテストの自動化を考えている方

## Playwrightとは？？
Playwrightとは一言で言うと、[Github](https://github.com/microsoft/playwright)には、
> Playwright は、Chromium、Firefox、WebKit を 1 つの API で自動化するためのNode.js ライブラリ

との記載があります。

[公式ドキュメント](https://playwright.dev/)がかなり充実していてそれを見るだけでも参考になりますのでぜひ読んでみてください。

## なぜPlaywright？
他のE2E自動テストのフレームワークもたくさんありますが、
> E2Eテストをただ自動化したいだけなんだ！

といった目的の場合は個人的にはPlaywrightをお勧めします。理由としては、
- たくさん便利な機能がある
- 複数のブラウザに対応している
- Visual testが簡単に実装できる

↓Playwrightと他のE2Eテストフレームワーク詳細にわかりやすく比較をしている記事がありましたので気になる方はご覧ください。

https://zenn.dev/taiga533/articles/f6e1ef07a8676e

## 便利な機能

ここからはPlaywrightの便利な機能を4つ紹介します。

### 1. コード自動生成

https://playwright.dev/docs/codegen

これは一番驚いた機能です。下のGIFを見ていただくとわかると思うのですが、普通に操作をするとその通りのコードを自動で生成してくれるという機能です。それをコピペして実際のコードとして貼り付けることができます。

起動コマンド
```
npx playwright codegen <開きたいページのURL>
```
Playwrightがデモ用で準備してくれているTODOリストのデモページ（ https://demo.playwright.dev/todomvc ）を開くとこんな感じになります。

![ezgif-1-734c52d180.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600371/5b2d720a-cc35-8785-5bbf-3036b0a6c223.gif)

左のウィンドウで操作をするとリアルタイムに右のウィンドウにその操作をコードで示してくれます。

**しかし、**

`hover`などのアクションはコードとして表示されないため注意が必要です。

### 2. Auto waiting

https://playwright.dev/docs/actionability

こちらが公式ドキュメントのURLです。
Auto waitingはPlaywrightの標準機能でして、簡単に言うと、

「DOMの目的の要素が操作できる状態になるまで自動で待ってくれる」

といった機能になります。

> トップ画面が表示された後、ログインボタンを押してログイン画面に移動をしたい

こういった実装をするときに、もしかすると以下のような問題があるかもしれません。

```
トップ画面が表示された後すぐに「ログインボタン」を押すと
まだログイン状態かどうかを確認している最中だったため、
ログインボタンのクリックが無効化されてしまった

↓ そのため...

「ログインボタン」を押す前に、
ログイン状態かの確認が終わるまで少し待つ処理を追加しなければいけない
```

といったように、コードでは厳密に記さないとうまく動かないことがあるかもしれません。しかしPlaywrightではこのような問題を自動で解決してくれる機能があり、それがAuto-waiting機能と言うことです。

Playwrightは要素の状態をそれぞれ担保していて、具体的には何か`click`や`hover`など要素を操作したい際には以下の主に5つの状態を確認して適したタイミングまで自動で待ってくれます。

#### 1. [Attached](https://playwright.dev/docs/actionability#attached)

要素がDocumentや[Shadowroot](https://developer.mozilla.org/ja/docs/Web/API/ShadowRoot)に追加されたとき

#### 2. [Visible](https://playwright.dev/docs/actionability#visible)
要素が目視できるとき
細かく言うと、
- `visibility:hidden`といったスタイルがついていないとき
- 要素の領域が空ではないとき

※以下の状態の要素はVisibleとはみなされません。
- 要素のサイズが0で見えないとき
- `display:none`のスタイルがついているとき

#### 3. [Stable](https://playwright.dev/docs/actionability#stable)
アニメーションされていないときか、もしくはアニメーションが完了したとき

#### 4. [Receives Events](https://playwright.dev/docs/actionability#receives-events)
他の要素によって覆い隠されていない時

#### 5. [Enabled](https://playwright.dev/docs/actionability#enabled)
`<button>`, `<select>`, `<input>` or `<textarea>`などに`disabled`属性がついていない時

* * *

このように、要素に対して`click()`や`hover()`などをする際に上記の状態を見て待ってくれます。
メソッドによってどの状態まで待つかと言うのは決まっていて、ドキュメントを見ていただくとわかりやすい表がありますのでぜひご確認ください。

https://playwright.dev/docs/actionability

### 3. スクリーンショット撮影
https://playwright.dev/docs/screenshots

Playwrightは３種類のスクリーンショットをすることができます。

表示画面のスクリーンショット
```typescript
await page.screenshot({ path: 'screenshot.png' });
```

ページ全体のスクリーンショット
```typescript
await page.screenshot({ path: 'screenshot.png', fullPage: true });
```

指定した要素のスクリーンショット
```typescript
await page.locator('.header').screenshot({ path: 'screenshot.png' });
```

また、引数の種類も豊富です。
- 画像フォーマット
- 撮影エリア
- 画質

などなど...

用途によって使い分けることができるのでとても便利です。

### 4. Visual test
https://playwright.dev/docs/test-snapshots

上記に示したスクリーンショット機能だけでなく、Playwrightはビジュアルテストもサポートしています。

実装も簡単に行うことができます。

ビジュアルテストでも細かくは2つあり、それらをそれぞれ紹介します。

#### `toHaveScreenshot()`

こちらはスクリーンショットを2回連続で同じ内容の画像が撮影されるまで連続して撮影をし、事前に準備している画像※を見比べて合っているかを確認することができます。

#### `toMatchSnapshot()`

こちらは事前に準備しているデータ※と同じデータかどうかを確認します。
画像ではなく、データというのがポイントです。
表示されているテキストデータなども比較してテストすることができます。

#### ※ 注意
上記で
> 事前に準備している画像・データを比較して...

といった記載しましたが、画像は事前に自前で準備するものではありません。
少し分かりづらいかもしれないですが、Playwrightで初回にテストを行った際に取得できる画像のことです。
そのため、そもそも初回で実行した結果が間違っていた場合、２回目以降では初回実行の結果を期待してしまうため、正しいものが表示されても初回で実行された結果が間違っていれば、テストが落ちてしまいます。

詳細はこちらのページでご確認いただければと思います。

https://playwright.dev/docs/test-snapshots


## 実装の例
Playwrightを用いた具体的な実装例を紹介します。

### テストシナリオ
まず、実装するテストシナリオです。このシナリオを元にコードを書いてみたいと思います。
Playwrihgtのドキュメントをテストしてみることにしてみましょう

> 1. ページに移動する
> 2. 「Locators」と検索をする
> 3. 目的のページが表示されることを確認する✅

### 実際のコード
上記のシナリオをコードに落とし込むと下記のようになります。

<details><summary><b>実際のコードを表示（折りたたみ）</b></summary><div>

```typescript
import { test, expect } from '@playwright/test';

test('hogehoge', async ({ page }) => {
  // playwrightのドキュメントページへ行く
  await page.goto('https://playwright.dev/');
  // ページのタイトルに'Playwright'という文字列が含まれていることを確認する✅
  await expect(page).toHaveTitle(/Playwright/);
  // 右上の検索ボタンをクリック
  await page.getByRole('button', { name: 'Search' }).click();
  // 「locators」と入力
  await page.getByPlaceholder('Search docs').fill('locators');
  // 検索結果のレスポンスが帰ってくるまで待つ
  await page.waitForResponse(response => {
    return response.url().indexOf("algolia.net") != -1
  })
  // Enterボタンを押して検索する
  await page.keyboard.press('Enter');
  // 表示されたページが「locators」関連のページかどうかを確認する✅
  await test.step("Verify whether displayed page is about locators", async () => {
    // ページのタイトルに'Locators'という文字列が含まれていることを確認する✅
    await expect(page).toHaveTitle(/Locators/);
    // ページのh1タグの文字列が"Locators"と表示されていることを確認する✅
    expect(await page.locator('h1').innerText()).toBe("Locators")
  })
});
```

</div></details>


以上のコードを実行し
```
npx playwright test
```

実行後、以下のコマンドでレポートを見ることができます。
```
npx playwright show-report
```
デフォルト設定では、
- chromium
- firefox
- webkit

以上の3つのブラウザでテストが行われるためそれぞれのレポートを見ることができます。
すると以下のようなレポートが表示されます。

<img width="600" alt="全体画面スクリーンショット" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600371/192a85a0-1c0c-17e9-5095-aa703831f5c8.png" >

chromiumの実行結果の詳細レポートを開くと以下の様にページが表示されます。

<img width="600" alt="スクリーンショット" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600371/84a9487e-65a3-bdc3-b125-d88fb7408ea7.png" >

失敗した場合もこの画面を確認するとどこで失敗してしまったのかを確認することができます。

## 実装時のコツ

実装をしていて開発しやすくなるコツなどを発見しましたので紹介します。

#### 行ごとに実行内容の説明をコメントアウトで書く

```typescript
  ...
  // playwrightのドキュメントページへ行く
  await page.goto('https://playwright.dev/');
  // ページのタイトルに'Playwright'という文字列が含まれていることを確認する✅
  await expect(page).toHaveTitle(/Playwright/);
  ...
```

複数の開発メンバーがいる際はプルリクレビューの際も潤滑にレビューを行ってもらうために、行ごとに実行内容をコメントアウトで書くとレビューがしやすくなりました。

#### まとめた共通処理を使う際は`test.step()`でその処理を囲む

```typescript
  test('hogehoge', async ({ page }) => {
    await test.step("search 1st time", async () => {
      await search(page); // <- 共通化した関数
    });
    await test.step("search 2nd time", async () => {
      await search(page); // <- 共通化した関数
    });
    await test.step("search 3rd time", async () => {
      await search(page); // <- 共通化した関数
    });
  });
```

テストケースが大量に増えてくると共通する処理は一つの関数にまとめたくなると思います。
ですが、例えば共通化をした関数内でtestが落ちてしまった際に、ログには

<img width="600" alt="stepなし" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600371/098a7427-d62d-cf43-0cb9-cbc28ebb1b02.png" >

このように表示されてしまうため、実際にどのステップで落ちたのかが見づらくなってしまいます。
そこで、その関数に`test.step()`をラップしてあげると以下の様にテストに失敗したステップがわかるため、スムーズにデバッグをすることができます。

<img width="600" alt="stepあり" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600371/72b1a288-cd00-8ca5-01cf-09149bb5e576.png" >

## 最後に

いかがでしたでしょうか？以上でPlaywrightの紹介を終わりたいと思います。
他にもgithubActionsと連携させciを組んだりする機能など、たくさん便利な機能があるので気になる方はドキュメントをぜひご覧ください。

https://playwright.dev/docs/ci-intro

## 参考文献
https://playwright.dev/

https://zenn.dev/taiga533/articles/f6e1ef07a8676e