Playwrightの導入からGitHub Actions上でテストを実行するまで


はじめに

ちょくちょく Terraform を触る機会が増えた です。最近、E2E テストフレームワークを調査する機会がありました。E2E テストフレームワークは Playwright 以外にも Cypress などいくつかありますが、個人的には Playwright が1番しっくりきました。今回は Playwright の導入から GitHub Actions 上で実行するところまでを試したので、やったことを備忘録的に残しておこうと思います。

PlayWrightとは

E2E テストを自動化するフレームワークです。

https://playwright.dev/

個人的にはこの辺りが魅力的だと感じました。

  • クロスブラウザ(WebKit も含む)をサポートしている
  • モバイルデバイスをエミュレート(ビューポートなど)してテストできる

https://playwright.dev/docs/emulation

構築していく!

テスト用にサンプルアプリを作る

今回は Vite と React でサンプルアプリを作成します。
まずは、下記のコマンドを実行しましょう。

yarn create vite

...

✔ Project name: … sample-app
✔ Select a framework: › react
✔ Select a variant: › react-ts

...

念の為、動作確認しておきます。

yarn dev

http://localhost:3000/ を開いて下記のように表示されれば OK です。
Hello Vite

次に、src/App.tsx を下記のように修正します。

import { useState } from 'react'
import logo from './logo.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
-        <p>Hello Vite + React!</p>
+        <p>{count}</p>
        <p>
          <button type="button" onClick={() => setCount((count) => count + 1)}>
-            count is: {count}
+            count up
          </button>
        </p>
        <p>
          Edit <code>App.tsx</code> and save to test HMR updates.
        </p>
        <p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
          {' | '}
          <a
            className="App-link"
            href="https://vitejs.dev/guide/features.html"
            target="_blank"
            rel="noopener noreferrer"
          >
            Vite Docs
          </a>
        </p>
      </header>
    </div>
  )
}

export default App

これでサンプルアプリの準備は終わりです。

Playwrightのセットアップ

Playwrightの導入

次は Playwright を導入していきましょう。
npm init playwright@latest を実行することで、Playwright の設定ファイルやサンプルの spec などが追加されます。後ほど、CI で Playwright を実行するので、GitHub Actions のワークフローも追加しておきましょう。

npm init playwright@latest

...

✔ Do you want to use TypeScript or JavaScript? · TypeScript
✔ Where to put your end-to-end tests? · src/tests
✔ Add a GitHub Actions workflow? (Y/n) · true

...

  yarn playwright test
    Runs the end-to-end tests.

  yarn playwright test --project=chromium
    Runs the tests only on Desktop Chrome.

  yarn playwright test src/tests/example.spec.ts
    Runs the tests of a specific file.

  yarn playwright test --debug
    Runs the tests in debug mode.

...

よく見ると、実行後のログに使い方が記載されていますね。

yarn playwright test はターゲットとしている全てのブラウザでテストします。
実行するブラウザは playwright.config.ts の下記に列挙されているので、
必要に応じて修正すると良さそうです。

playwright.config.ts
const config: PlaywrightTestConfig = {

...

  /* Configure projects for major browsers */
  projects: [
    {
      name: 'chromium',
      use: {
        ...devices['Desktop Chrome'],
      },
    },

    {
      name: 'firefox',
      use: {
        ...devices['Desktop Firefox'],
      },
    },

    {
      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: {
    //     channel: 'msedge',
    //   },
    // },
    // {
    //   name: 'Google Chrome',
    //   use: {
    //     channel: 'chrome',
    //   },
    // },
  ],

...

};

export default config;

yarn playwright test --project=chromium は指定したブラウザでテストを実行します。
個人的にこの2つのコマンドはよく使いそうだなと思ったので、npm scripts に登録しました。

package.json
{
  "name": "sample-app",
  "private": true,
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
+   "e2e:all": "playwright test",
+   "e2e:chromium": "playwright test --project=chromium",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "devDependencies": {
    "@playwright/test": "^1.20.2",
    "@types/react": "^17.0.33",
    "@types/react-dom": "^17.0.10",
    "@vitejs/plugin-react": "^1.0.7",
    "typescript": "^4.5.4",
    "vite": "^2.9.0"
  }
}

これでテストを実行できるようになったはずなので、試しにテストを流してみましょう。

yarn e2e:all

...

Running 75 tests using 3 workers

  75 passed (13s)

To open last HTML report run:

  npx playwright show-report

✨  Done in 13.40s.

うまく実行できたようです🎉

サンプルアプリのテストを追加する

今回は次の操作をした時の振る舞いをテストしてみます。

  1. count up ボタンを押す
  2. count が増加していること

まずは空の spec ファイルを追加しましょう。

touch src/tests/countUp.spec.ts

次に上記を確認するテストを追加します。

src/tests/countUp.spec.ts
import { test, expect } from '@playwright/test';

// 各テストを実行する前に http://localhost:3000 に遷移する
test.beforeEach(async ({ page }) => {
  await page.goto('http://localhost:3000');
});

test('カウントアップボタンをクリックすると、カウントが増加すること', async ({ page }) => {
  // data-testid 属性が count-up-button な要素をクリックする
  await page.locator('data-testid=count-up-button').click();
  // data-testid 属性が count な要素のテキストが1になっていることを確認
  await expect(page.locator('data-testid=count')).toContainText('1');
});

Playwright は data-testid 属性をサポートしているため、要素を data-testid=hoge のような形で選択できます。他にもサポートしている属性があるので、気になる方はこちらを参照ください。

https://playwright.dev/docs/selectors#id-data-testid-data-test-id-data-test-selectors

次はアプリケーション側で該当の要素に data-testid 属性を付与していきます。

src/App.tsx
import { useState } from 'react'
import logo from './logo.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
-       <p>{count}</p>
+       <p data-testid="count">{count}</p>
        <p>
-         <button type="button" onClick={() => setCount((count) => count + 1)}>
+         <button type="button" data-testid="count-up-button" onClick={() => setCount((count) => count + 1)}>
            count up
          </button>
        </p>
        <p>
          Edit <code>App.tsx</code> and save to test HMR updates.
        </p>
        <p>
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
          {' | '}
          <a
            className="App-link"
            href="https://vitejs.dev/guide/features.html"
            target="_blank"
            rel="noopener noreferrer"
          >
            Vite Docs
          </a>
        </p>
      </header>
    </div>
  )
}

export default App

これでサンプルアプリのテストを実行する準備が整いました。
今回は http://localhost:3000 に遷移してテストを実行するので、
yarn dev した後に yarn e2e:all src/tests/countUp.spec.ts を実行してみましょう。(後ほど設定を追加することで、yarn dev の立ち上げは不要になります)

yarn e2e:all src/tests/countUp.spec.ts
...

Running 3 tests using 3 workers
[1/3] [webkit] › countUp.spec.ts:7:1 › カウントアップボタンをクリックすると、カ
[2/3] [chromium] › countUp.spec.ts:7:1 › カウントアップボタンをクリックすると、
[3/3] [firefox] › countUp.spec.ts:7:1 › カウントアップボタンをクリックすると、

  3 passed (1s)

...

どうやらテストがパスしたようです。
念の為、わざとテストを失敗させてみましょう。

src/tests/countUp.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('http://localhost:3000');
});

test('カウントアップボタンをクリックすると、カウントが増加すること', async ({ page }) => {
  await page.locator('data-testid=count-up-button').click();
- await expect(page.locator('data-testid=count')).toContainText('1');
+ await expect(page.locator('data-testid=count')).toContainText('2');
});

この状態で再度テストを流してみます。

yarn e2e:all src/tests/countUp.spec.ts
...

[1/3] [webkit] › countUp.spec.ts:7:1 › カウントアップボタンをクリックすると、カ
  1) [webkit] › countUp.spec.ts:7:1 › カウントアップボタンをクリックすると、カウントが増加すること ===============================

    Error: expect(received).toContainText(expected)

    Expected string: "2"
    Received string: "1"

...

ちゃんとエラーになりましたね👍
確認が終わったら戻してテストが通るようにしておきましょう。

GitHub Actions 上でテストを実行する

前準備

先ほどローカルで試した時は、yarn dev した後にテストを流していました。CI 上でも同様の方法でテストしようとすると、テスト用に別途サーバーを立ち上げる必要があり面倒です...
でも安心してください、Playwright にはテスト実行前に dev server を立ち上げる設定があります🙌

playwright.config.ts
const config: PlaywrightTestConfig = {
...

  /* Run your local dev server before starting the tests */
-   // webServer: {
-   //   command: 'npm run start',
-   //   port: 3000,
-   // },
+  webServer: {
+    command: 'yarn dev',
+    port: 3000,
+  },
...
};

これで、テスト実行前に webServer が立ち上がるので、yarn dev を明示的に実行しなくて良くなります。この状態でテストを流すと yarn dev を立ち上げなくてもテストできていることが確認できると思います。

ワークフローを修正する

次はワークフローを修正していきます。

.github/workflows/playwright.yml
name: Playwright Tests
- on:
-  push:
-    branches: [ main, master ]
-  pull_request:
-    branches: [ main, master ]
+ on: push
jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v2
      with:
        node-version: '14.x'
    - name: Install dependencies
      run: yarn
    - name: Install Playwright
      run: npx playwright install --with-deps
    - name: Run Playwright tests
-     run: yarn playwright test
+     run: yarn e2e:all
    - uses: actions/upload-artifact@v2
      if: always()
      with:
        name: playwright-report
        path: playwright-report/
        retention-days: 30

変更したのは次の2点です。

  • main or master ブランチ以外を push した時もワークフローを実行する
  • npm scripts を実行する

これまでの作業を commit & ブランチを push してみます。
ちゃんとテストが実行されていますね🎉

GitHub Actionsのログ

これで Playwright の導入から GitHub Actions 上でテストを実行するところまで終わりました。
おつかれさまでした!!

終わりに

今回は push 時に全てのブラウザでテストしていますが、毎回これだと時間がかかりすぎるので、いずれ下記のような形に修正したいと思っています。

  • push 時 -> どれか 1 つのブラウザでのみテスト
  • 週1のビルド時 -> サポートしている全てのブラウザでテスト

試したらまた記事にしようと思います!

参考