🔚

pnpmとniを用いてPlaywrightでE2Eテスト、VRTをする環境を整えよう(GitLabCIもオマケで)

2023/04/15に公開

はじめに

初心者フロントエンドエンジニアをしているRimlと申します。
お久しぶりです。

ふと弊社の分報SlackチャンネルでPlaywrightの話題が上がり、個人的に触れてたのでその知見や溜め込んでた記事は共有したのですが、どうせなら自分の関わってるプロダクトに導入すればいいじゃん!という流れから勝手ながら改善活動として環境構築の方をしました。

何気なくツイートしたら反応がちょっとあった(?)
https://twitter.com/Fande4d/status/1645761599200989184?s=20

のもあり備忘録にでもなったらいいなと言うことで今回投稿させていただきました!

E2Eテストはローカル環境で行わないためリポジトリを分けて構築します。

注意書き

以下環境にて構築、動作するものを前提にしています。
それぞれ違うものは置き換えて読み進めていただけると幸いです。

環境

  • MacOS Ventura 13
  • VSCode
  • Gitlab Runner(Ubuntu 22.04.2 LTS)
  • Nodejs 18.x

導入編

まず前提で導入するものを入れていきます。

  • pnpm@latest-8

pnpmはディスク容量を効率化してくれたりnpmやyarnより厳格なパッケージ管理をしてくれるPackage Managerです。

npm install -g pnpm
  • ni

ni は

  • npm
  • yarn
  • pnpm
  • bun

のlockfileを読み取って、適切なコマンドを実行してくれるPackage Managerです。
打つコマンドを固定できて、とても便利です。

npm i -g @antfu/ni

playwrightの環境を作ります。

mkdir e2e-test
cd e2e-test

pnpm dlx create-playwright

Library/pnpm/store/v3/tmp/dlx-52858      |   +1 +
Library/pnpm/store/v3/tmp/dlx-52858      | Progress: resolved 1, reused 1, downloaded 0, added 1, done
Getting started with writing end-to-end tests with Playwright:
Initializing project in '.'
? Do you want to use TypeScript or JavaScript? …
❯ TypeScript
  JavaScript

TypeScriptを選びましょう

Where to put your end-to-end tests? › tests

e2e testを配置するディレクトリ名を記述します、デフォルトはtestsで今回はそのままで進めていきます。

? Add a GitHub Actions workflow? (y/N)false

今回はGitLabCI前提なのでfalseにします。

? Install Playwright browsers (can be done manually via 'pnpm exec playwright install')? (Y/n)true

playwrightをいれてくれるやつです。
デフォルト通りtrueでOKです。

✔ Success! Created a Playwright Test project at 
~/e2e-test

このように出たら完了です。

@types/node、dotenvもいれましょう。

ni @types/node -D
ni dotenv

VSCode等で開きましょう

code .

pnpmを固定するためにcorepackも設定しましょう。

corepack enable

package.jsonに "packageManager"を追記します。

package.json
{
  "packageManager": "pnpm@8.2.0"
}

実行しやすいようにscripsも書きましょう

package.json
"scripts": {
    "test": "pnpm exec playwright test",
    "test:ui": "pnpm exec playwright test --ui",
    "test:chromium": "pnpm exec playwright test --project=chromium",
    "test:debug": "pnpm exec playwright test --debug",
    "test:gen": "pnpm exec playwright codegen"
  },

niの設定ファイルを書きましょう

.nirc
# lockfileが存在しない場合に使用するPackage Manager
defaultAgent=pnpm

# グローバルインストールする場合に使用するPackage Manager
globalAgent=pnpm

.gitignoreを書きましょう

.gitignore
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/
/tests/screenshots/
storageState.json
.env

.envファイルに環境変数を書いていきます。

.env
TEST_URL=テスト先のURL
TEST_LOGIN_URL=アプリケーションにログイン機能がある場合のログイン先URL
TEST_LOGIN_REDIRECT_URL=ログイン後のリダイレクトURL
TEST_ACCOUNT_MAIL=ログインする際のメールアドレス
TEST_ACCOUNT_PASS=ログインする際のパスワード

本編

Playwrightの設定、テストを記述する

今回は認証設定、スクリーンショットテスト、VRTを書いていきます。

以下自動生成されたPlaywrightのconfigファイルです
こちらを編集していきます。

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

/**
 * Read environment variables from file.
 * https://github.com/motdotla/dotenv
 */
// require('dotenv').config();

/**
 * See https://playwright.dev/docs/test-configuration.
 */
export default defineConfig({
  testDir: './tests',
  // リトライ時の動画などを排出するディレクトリを指定する
+ outputDir: "test-results/",
  // タイムアウトを伸ばしておく
+ timeout: 100 * 1000,
  /* Run tests in files in parallel */
  fullyParallel: true,
  /* Fail the build on CI if you accidentally left test.only in the source code. */
  forbidOnly: !!process.env.CI,
  /* Retry on CI only */
  // リトライ回数は1回にしときます
- retries: process.env.CI ? 2 : 0,
+ retries: process.env.CI ? 1 : 0,
  /* Opt out of parallel tests on CI. */
  workers: process.env.CI ? 1 : undefined,
  /* Reporter to use. See https://playwright.dev/docs/test-reporters */
  reporter: 'html',
  /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
  use: {
    /* GUIが表示されないように設定 */
+   headless: true,
    /* Base URL to use in actions like `await page.goto('/')`. */
    // baseURL: 'http://127.0.0.1:3000',

    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
-   trace: 'on-first-retry',
    // リトライ時トレース、動画を排出するようにしておく
+   trace: process.env.CI ? "on-first-retry" : "on",
+   video: process.env.CI ? "on-first-retry" : "on",
  },

  /* Configure projects for major browsers */
  projects: [
    // 認証用の設定
 +  { name: "setup", testMatch: /.*\.setup\.ts/ },
    // chromium、firefoxでテストを行うようにする
    {
      name: 'chromium',
      use: {
          ...devices['Desktop Chrome'],
	  // ロケールを日本語に設定
+         locale: "ja-JP",
          // 認証情報読み取り
+         storageState: "./storageState.json",
      },
      // 認証
+     dependencies: ["setup"],
    },

    {
      name: 'firefox',
      use: { 
          ...devices['Desktop Firefox'],
+         locale: "ja-JP", // ロケールを日本語に設定
+         storageState: "./storageState.json",	  
      },
+     dependencies: ["setup"],
    },

    // {
    //  name: 'webkit',
    //  use: { ...devices['Desktop Safari'] },
    // },

    /* Test against mobile viewports. */
    // {
    //   name: 'Mobile Chrome',
    //   use: { ...devices['Pixel 5'] },
    // },
    // {
    //   name: 'Mobile Safari',
    //   use: { ...devices['iPhone 12'] },
    // },

    /* Test against branded browsers. */
    // {
    //   name: 'Microsoft Edge',
    //   use: { ...devices['Desktop Edge'], channel: 'msedge' },
    // },
    // {
    //   name: 'Google Chrome',
    //   use: { ..devices['Desktop Chrome'], channel: 'chrome' },
    // },
  ],
+  expect: {
    // テストのタイムアウト設定
+    timeout: 5000,
    // VRT での許容範囲の設定
    // 異なるピクセルが存在する可能性のある許容量を示す
+    toHaveScreenshot: {
+      maxDiffPixels: 10,
+    },

    // VRT での許容範囲の設定
    // 画素の総量に対して、異なる画素の比率許容
+    toMatchSnapshot: {
+      maxDiffPixelRatio: 10,
+    },
+  },

  /* Run your local dev server before starting the tests */
  // webServer: {
  //   command: 'npm run start',
  //   url: 'http://127.0.0.1:3000',
  //   reuseExistingServer: !process.env.CI,
  // },
});

認証用の設定

testsディレクトリ配下に auth.setup.ts ファイルを作成し、ログインフローを書いていきます。
以下は一例です。
それぞれアプリケーションのログインフローに合わせて記述しましょう。

こちらを記述する際にPlaywrightの売りの一つでもある、codegenを使うと便利です。

nr test:gen
cd tests
touch auth.setup.ts
auth.setup.ts
import { test as setup } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config();

const authFile = "storageState.json";

setup("ログイン認証", async ({ page }) => {
  // 認証情報を環境変数から取得
  const MAIL = process.env.TEST_ACCOUNT_MAIL ?? "";
  const PASS = process.env.TEST_ACCOUNT_PASS ?? "";
  const TEST_URL = process.env.TEST_URL ?? "";
  const LOGIN_URL = process.env.TEST_LOGIN_URL ?? "";
  const LOGIN_REDIRECT_URL = process.env.TEST_LOGIN_REDIRECT_URL ?? "";
  console.log(`環境変数確認(テストURL): ${TEST_URL}`);
  // 認証ステップ
  await page.goto(LOGIN_URL);
  await page.getByPlaceholder("メールアドレス").fill(MAIL);
  await page.getByPlaceholder("パスワード").fill(PASS);
  await page.getByRole("button", { name: "ログイン" }).click();

  console.log(`認証URL: ${LOGIN_REDIRECT_URL}`);
  // ページがCookieを受け取るまで待つ。
  // ログインフローでは、何度かリダイレクトする過程でCookieを設定することがあります。
  await page.waitForURL(LOGIN_REDIRECT_URL);
  console.log("認証完了");
  // 実際にCookieが設定されていることを確認するため、最終的なURLを待ちます。
  await page.goto(`${TEST_URL}/`);
  await page.waitForTimeout(5000);
  console.log("cookie設定完了");

  // 認証ステップを終了
  await page.context().storageState({ path: authFile });
});

ここまで出来たら事前準備パート完了です。

テストを記述

スクリーンショットテストを書く

screen-shot.spec.ts
import { test } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config();

test.describe("URLにアクセステスト", () => {
  const TEST_URL = process.env.TEST_URL ?? "";
  const outDir = "tests/screenshots/url";

  test.describe("メイン画面", () => {
    test("/ ページにアクセスできるか", async ({ page }, testInfo) => {
      await page.goto(`${TEST_URL}/`);
      // ページ読み込み待機
      await page.waitForTimeout(5000);
      const screenshot = await page.screenshot({
        path: `${outDir}/index.png`,
        fullPage: true,
      });
      // レポートで画像を表示するためにアタッチする
      await testInfo.attach("index", {
        body: screenshot,
        contentType: "image/png",
      });
    });

    test("/about ページにアクセスできるか", async ({
      page,
    }, testInfo) => {
      await page.goto(`${TEST_URL}/about`);
      await page.waitForTimeout(5000);
      const screenshot = await page.screenshot({
        path: `${outDir}/about.png`,
        fullPage: true,
      });
      // レポートで画像を表示するためにアタッチする
      await testInfo.attach("about", {
        body: screenshot,
        contentType: "image/png",
      });
    });
});

VRTを書いていきます。

vrt.spec.ts
import { test, expect } from "@playwright/test";
import dotenv from "dotenv";
dotenv.config();

test.describe("VisualRegressionTest", () => {
  const TEST_URL = process.env.TEST_URL ?? "";

  test.describe("メイン画面", () => {
    test("/ ページに差分がないか", async ({ page }) => {
      await page.goto(`${TEST_URL}/`);
      await page.waitForTimeout(5000);
      await expect(page).toHaveScreenshot("index.png");
    });

    test("/about ページに差分がないか", async ({
      page,
    }) => {
      await page.goto(`${TEST_URL}/about`);
      await page.waitForTimeout(5000);
      await expect(page).toHaveScreenshot("about.png");
    });
  });
});

VRTの場合、事前にsnapshotが必要になります。
今回CI前提でVRTを回していくため同じサーバと同じ環境のものが必要になります。
今回はLinux上でGitLab Runnerが動いているためDockerにて生成します。

# CIと同じLinux環境で行う
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:focal /bin/bash
# docker内ではpnpmを使用、環境を用意するのがめんどくさいためnpxを使用
npx playwright test --update-snapshots
exit

こちらで生成した snapshot は ページ名-chromium-linux.png または ページ名-firefox-linux.png と出力されます。
こちらは変更前のベースの画像となるためGitLabにコードと一緒に push しましょう。

GitLab CIの設定

CIファイルを作成します。

gitlab-ci.yml
default:
  image: mcr.microsoft.com/playwright:focal

stages:
  - 🧪 e2e
  - 📄 publish

cache:
  paths:
    - node_modules/
    - ~/.cache/ms-playwright
  key: npm_$CI_COMMIT_REF_SLUG

# pnpmのコマンドを利用できるように前処理を行う
.install:
  before_script:
    - corepack enable
    - corepack prepare pnpm@latest-8 --activate
    - pnpm install
  artifacts:
    paths:
      - node_modules
      - ~/.cache/ms-playwright

# E2Eテストを行う
e2e-test:
  stage: 🧪 e2e
  extends: .install
  script:
    - pnpm test
  artifacts:
    paths:
      - test-results/
      - playwright-report/
      - tests/screenshots/
    expire_in: 3 days

# E2Eの結果レポートをpagesにホスティングする
pages:
  image: alpine:latest
  stage: 📄 publish
  script:
    - mv playwright-report public
  when: always
  artifacts:
    paths:
      - public
  cache: {}

これで一連の流れの完了です。

あとはローカルで

nr test

を実行したりGitLabにデプロイし動かしてみましょう。

まとめ

さらっと環境構築、一連の流れの方を書いてみました。
結構調べながら環境構築すると時間がかかったりするので手助けになれたら嬉しいです!

今後はQA視点でシナリオを書きながらテストを行える「Gauge」というツールを知ったので今回のPlaywrightのリポジトリに導入し勉強したり、よりよいフロントエンドテストを書けるようにがんばります!

こちらの本、個人的にめっちゃ楽しみにしてます。
https://zenn.dev/takepepe/articles/frontend-testing-book

最後まで読んでいただきありがとうございました!

Discussion