pnpmとniを用いてPlaywrightでE2Eテスト、VRTをする環境を整えよう(GitLabCIもオマケで)
はじめに
初心者フロントエンドエンジニアをしているRimlと申します。
お久しぶりです。
ふと弊社の分報SlackチャンネルでPlaywrightの話題が上がり、個人的に触れてたのでその知見や溜め込んでた記事は共有したのですが、どうせなら自分の関わってるプロダクトに導入すればいいじゃん!という流れから勝手ながら改善活動として環境構築の方をしました。
何気なくツイートしたら反応がちょっとあった(?)
のもあり備忘録にでもなったらいいなと言うことで今回投稿させていただきました!
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"を追記します。
{
"packageManager": "pnpm@8.2.0"
}
実行しやすいようにscripsも書きましょう
"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の設定ファイルを書きましょう
# lockfileが存在しない場合に使用するPackage Manager
defaultAgent=pnpm
# グローバルインストールする場合に使用するPackage Manager
globalAgent=pnpm
.gitignoreを書きましょう
node_modules/
/test-results/
/playwright-report/
/playwright/.cache/
/tests/screenshots/
storageState.json
.env
.envファイルに環境変数を書いていきます。
TEST_URL=テスト先のURL
TEST_LOGIN_URL=アプリケーションにログイン機能がある場合のログイン先URL
TEST_LOGIN_REDIRECT_URL=ログイン後のリダイレクトURL
TEST_ACCOUNT_MAIL=ログインする際のメールアドレス
TEST_ACCOUNT_PASS=ログインする際のパスワード
本編
Playwrightの設定、テストを記述する
今回は認証設定、スクリーンショットテスト、VRTを書いていきます。
以下自動生成されたPlaywrightのconfigファイルです
こちらを編集していきます。
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
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 });
});
ここまで出来たら事前準備パート完了です。
テストを記述
スクリーンショットテストを書く
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を書いていきます。
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ファイルを作成します。
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のリポジトリに導入し勉強したり、よりよいフロントエンドテストを書けるようにがんばります!
こちらの本、個人的にめっちゃ楽しみにしてます。
最後まで読んでいただきありがとうございました!
Discussion