Playwrightで自社製品のテストを書いてみた

# 自己紹介

初めまして。株式会社マーズフラッグの小池といいます。
フロントエンドエンジニアとして在籍しておりまして、マーズのエンジニアになってから1年が経とうとしています。
マーズでは主に[**MARS FINDER**](https://www.marsflag.com/ja/services/marsfinder/)という自社製品のメンテナンスのほか、それに付随した新機能の実装を担当しています。

今回は初めてマーズがアドベントカレンダーに参加するということで記事を書かせていただくことになりました。

# Playwright導入の背景
ここ半年間、自社製品のバグやお客様からのご要望で機能の修正などを行っていますが、そこで課題になったのが**リリース前のE2Eテスト**です。製品を修正した場合に人の手でテストを行ってきましたが、そのテストが簡易的ではあるのですがかなりの量で、人の手で行うと30分から1時間かかってしまいます。

そこで最近自動化できたら効率がいいな〜と思いまして、初めてテストの自動化に試みました。
私自身テスト実装が初めてだったので、手を動かし知見を溜めつつ、実装を行なっていきました。
この記事で自社製品がどのようなもので、どういったテストを行っているかの一部をご紹介しながら、playwrightの魅力について書いていこうかと思います！

# Playwrightとは

https://playwright.dev/

> Playwrightは、エンドツーエンドテストのニーズに対応するために特別に作成されました。Playwrightは、Chromium、WebKit、Firefoxを含むすべての最新のレンダリングエンジンをサポートしています。Windows、Linux、macOS上で、ローカルまたはCI上で、Android用Google ChromeおよびMobile Safariのネイティブモバイルエミュレーションを使用してヘッドレスまたはヘッディングでテストできます。

要はE2Eのテストを自動で行ってくれるというものです。
Playwright以外にもjestなどのテストツールもありますが、今回はふと目にしたPlaywrightを使っていこうかと思いました。

# MARS FINDERとは
https://www.marsflag.com/ja/services/marsfinder/

マーズフラッグが誇るWebサイトの能力を最大限に活用するためのサイト内検索サービスとなっています！笑
実際にplatform上でお手続きをしていただいてタグを設置するだけで使えるシンプルなサイト内検索機能となっています。
この製品のフロント部分のテストを今回行っていこうというわけです。


# 実際のテストの一例

今回紹介するのは
"**ユーザーが検索を行って、画面の表示崩れなどがないか**"
を判断するスナップショット機能を利用したテストです。


## Playwright導入

導入はすごく簡単で
```zsh
pnpm create playwright
```
をプロジェクト内で実行するだけです。

```zsh
✔ Where to put your end-to-end tests? · tests
✔ Add a GitHub Actions workflow? (y/N) · false
✔ Install Playwright browsers (can be done manually via 'pnpm exec playwright install')? (Y/n) · true
```
色々聞いてきますが、全部Enterでいいと思います。
そうするとtestsフォルダの中にexample.spec.tsというファイルが出来上がってるほか、playwright.config.tsなども出来上がっているかと思います。


```zsh
npx playwright test
```

実際にテストを走らせてみますと、example.spec.tsに書いてあるPlaywrightサイトへ行ってタイトルがあるか、Get startedリンクボタンはあるかなどのテストが走ります。

```zsh
npx playwright show-report
```

また上記コマンドで、テスト結果を見ることができます。
![スクリーンショット 2023-11-17 11.23.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583896/c3f716c3-e5dc-b901-fdbb-e59e332e2bee.png)

便利ですね！！
これで準備はOKだと思います！次はテストコードの書き方です。


## Playwrightのテストコード

際ほども記述した通り、"**ユーザーが検索を行って、画面の表示崩れなどがないか**"をテストしていきます。
実際に私はテスト環境を用意して実装しました。
下記gifが想定しているユーザーの動きです。
![画面収録.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583896/1881bedb-0d9c-9b7e-7a8a-bdc9cdae1092.gif)
(画質が悪くてすみません、、、。)

このような場合のテストを書いていきます。

:::note warn
注意
少し誤りがあるかもしれませんので、ご指摘いただけたらありがたいです。
:::

### コード概要(よく使う関数を定義しました。)

#### imageのレンダリングを待つ関数
```ts
const waitForImageLoad = async (page: Page) => {
  for (const img of await page.getByRole("img").all()) {
    await expect(img).toHaveJSProperty("complete", true);
    await expect(img).not.toHaveJSProperty("naturalWidth", 0);
  }
};
```

#### pageのスナップショットを撮るテスト関数
```ts
const takeSnapShot = async (page: Page, name: string) => {
  expect(await page.screenshot({ fullPage: true })).toMatchSnapshot([
    name,
    `${name}.png`,
  ]);
};
```

#### 要素がレンダリングされるかどうかのテスト関数
```ts
const checkSingleElementRender = async (
  page: Page,
  name: string,
  comment?: string
) => {
  await page
    .locator(`.${name}`)
    .waitFor({ timeout: 2000 }
    .then(() => {
      comment
        ? console.log(`${comment}は表示されています。`)
        : console.error(`${name}表示OK`);
    })
    .catch((error: any) => {
      comment
        ? console.error(`${name}が表示されていません。`, error)
        : console.error(`${name}表示NG`);
    });
};
```

#### 複数要素がレンダリングされるかどうかのテスト関数
```ts
const checkMultipleElementRender = async (
  page: Page,
  name: string,
  comment?: string
) => {
  await page
    .$$(`.${name}`)
    .then(() => {
      comment
        ? console.log(`${comment}は表示されています。`)
        : console.error(`${name}表示OK`);
    })
    .catch((error: any) => {
      comment
        ? console.error(`${name}が表示されていません。`, error)
        : console.error(`${name}表示NG`);
    });
};
```

#### 検索やその他ページ遷移やページングなどを行った時にページが完全に表示されるのを待つ関数
```ts
const waitForPageLoadAndSearchResultsAndFeaturedContents = async (
  page: Page
) => {
  // ページの読み込みを待つ
  await page.waitForLoadState("load");

  console.log("ページの読み込み完了");

  // ページ遷移後に検索結果が表示されるまで待機
  await page.locator(".mf_finder_organic_wrapper").waitFor();

  console.log("検索結果の表示完了");

  // ページ遷移後におすすめコンテンツが表示されるまで待機
  await page.locator(".mf_finder_featured_contents_wrapper").waitFor();

  console.log("おすすめコンテンツの表示完了");
};
```


### コード概要(走らせるテストの本体)

```ts
import { test, expect, Page } from "@playwright/test";

//　どこのページをテストするか
const pagePathForSnapShotTest = [
  // ここに各ページの情報を追加
  {
    name: "test1",
    path: `${baseUrl}test-1/`,
  },
  {
    name: "test2",
    path: `${baseUrl}test-2/`,
  },
];

// テスト本体
test.describe.parallel("Checking all functions and drawing", () => {
  pagePathForSnapShotTest.map((item) => {
    test(`snapshot test ${item.name}`, async ({ page }) => {
      await page.goto(item.path);
      await page.waitForLoadState("load");

      // 検索ボックスに入力
      await page.getByPlaceholder("検索ボックス")?.click();
      await page.getByPlaceholder("検索ボックス")?.fill("mars");

      // suggestが出るかどうかをテスト
      await checkSingleElementRender(
        page,
        "mf_finder_searchBox_suggest_items",
        "サジェスト"
      );

      // 検索ボタンを押す
      await page.locator(".mf_finder_searchBox_submit")?.click();

      // pageの読み込みを待つ
      await waitForPageLoadAndSearchResultsAndFeaturedContents(page);

      // 画像の読み込みを待つ
      await waitForImageLoad(page);

      // mf_finder_mark(ハイライト)が存在するかどうかを確認
      await checkMultipleElementRender(page, "mf_finder_mark", "ハイライト");

      // スクリーンショットを撮り、前回のものと比較
      await takeSnapShot(page, `${item.name}_snapshot`);
    });
  });
});
```


## テスト実行結果(1回目)

```zsh
npx playwright test
```
でテスト実行。下記画像が結果。

![初回実行時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583896/d02b1b6a-8536-9ec1-9e46-8bff2399b483.png)

全部失敗！！笑
ですがこれは比較するsnapshotがないのでerrorになっているというわけです。
ここで撮ったsnapshotと2回目以降比べて差異があるかどうかを確認するので、1回目はあらかじめ実行しておく必要があります。
ちなみにこのような感じでスナップショットを撮ることができてます。(tests/example.spec.ts-snapshots配下に保存されています。)
![snapshot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583896/bc87da26-9aa7-e85a-b46c-60dbca0af262.png)


## テスト実行結果(2回目)
ここからがテスト本番です。
機能変更などをした後実行したとしましょう！



![2回目実行時.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583896/ed27da47-5de8-72d5-c07a-ba10e79132f3.png)

オールOKです。
1回目に実行したスナップショットと比べてズレがないことが確認できました。


# 最後に
E2Eのテストは考えることが多く、また機能の実装の仕方によってテストの書き方が違ってくるので、相当な労力を使うことがわかりました。(selectOptionなどの書き方などが例に挙げられます。)
しかし、自社サービスや継続して使う製品についてはやる価値は大いにあることもわかりました。

現在テスト自動化は実装真っ只中ですが、このように学びながら作り、その知識をアウトプットするいい機会をいただけました。
またテストの進捗があればこちらの記事に追加するほか、別の記事にしてアウトプットしていこうと思っていますので、引き続きよろしくお願いいたします。

ありがとうございました。

zenn: https://zenn.dev/tatausuru

# おまけ 
## Best Practices

https://playwright.dev/docs/best-practices

↓以下翻訳↓

**ロケータを使う**
エンドツーエンドのテストを書くには、まずウェブページ上の要素を見つける必要があります。これを行うには、Playwrightに組み込まれているロケータを使用する。ロケーターには自動待機とリトライ機能が付いている。自動待機とは、Playwrightがクリックを実行する前に、要素が表示されていて有効になっていることを確認するなど、要素に対してさまざまな実行可能性チェックを行うことを意味します。テストに弾力性を持たせるために、ユーザー向けの属性と明示的な契約を優先することをお勧めします。


**ロケータの生成**
Playwrightには、テストを生成してロケータを選択するテストジェネレータがあります。ページを見て、ロール、テキスト、テスト ID ロケータを優先して、最適なロケータを決定します。ロケータにマッチする要素が複数見つかった場合は、ロケータを改良して弾力性を持たせ、ターゲット要素を一意に識別できるようにします。


**ウェブ・ファーストのアサーションを使う**
アサーションは、期待する結果と実際の結果が一致するかどうかを検証するための方法です。ウェブファーストアサーションを使用することで、Playwright は期待する条件が満たされるまで待機する。例えば、アラートメッセージをテストする場合、テストはメッセージを表示させるボタンをクリックし、アラートメッセージがあるかどうかをチェックする。アラートメッセージが表示されるまでに 0.5 秒かかる場合、toBeVisible() などのアサーションは待機し、必要に応じて再試行します。


**デバッグの設定**
ローカルデバッグのためには、VS Code エクステンションをインストールして、VSCode でテストをライブデバッグすることをお勧めします。デバッグモードでテストを実行するには、実行したいテストの横の行を右クリックしてブラウザウィンドウを開き、ブレークポイントが設定された場所で一時停止します。


**Playwrightの依存関係を最新に保つ**
Playwrightのバージョンを最新に保つことで、最新のブラウザバージョンでアプリをテストし、最新のブラウザバージョンが一般公開される前に不具合を検出することができます。

-----

>参考資料

https://github.com/microsoft/playwright

https://zenn.dev/012xx/articles/c3839d1f631829

https://zenn.dev/t_poyo/articles/4c47373e364718

https://zenn.dev/toono_f/scraps/2368d03dacca93

https://zenn.dev/ryoka419319/articles/428ecee482a8ea

https://zenn.dev/emmatester/articles/379f76222d0cda

https://zenn.dev/aokiken/scraps/2bb299c6c9f534