🎭

E2E テストの決定版! テスト開発の効率が爆上がりする Playwright (TypeScript版)

2023/07/10に公開

E2E テストスイート Playwright はマイクロソフトが提供しているだけあって Visual Studio Code と高いレベルで統合されています。

E2E テストをステップ実行することができ、変数の値を参照(インスペクト)できます。もちろん、ブラウザーの様子も同時に確認できます。

現在テストがどこを実行中なのかがテストコードにリアルアイムに強調表示されるので、どこで意図せずに止まっているかがすぐに分かります。この機能は追うのが難しい非同期処理にも対応しているので非常に助かります。

テストを動かさなくてもコードにマウスを合わせるだけで、対応する GUI 部品がどこにあるかがすぐに分かるようにブラウザー内に強調表示されます。

このように高度な開発環境が使えるため、他の E2E テスト スイート に比べて開発効率が格段に上がります。

本書で説明しているコードやプロジェクトは、下記の GitHub に置いてあります。

https://github.com/Takakiriy/Trials/tree/master/try_Playwright/try_Playwright_1_35/tests

Playwright のインストール

以後のインストール手順については、Windows 11 22H2 で動作確認しています。

Node.js をインストールします

Playwright が使う Node.js をインストールします。

https://nodejs.org/ja/downloadをブラウザーで開き、Windows Installer (.msi) >> 64-bit をダウンロードして、実行します。もしくは、
https://nodejs.org/ja/download/releases をブラウザーで開き、Releases >> .msi をダウンロードして、実行します。ダウロードするファイルの名前は node-v18.16.1-x64.msi などです。ちなみに、CI 環境が古い Linux (CentOS7) の場合 Node.js 18 では動かないようです。

インストール オプションはすべてデフォルトで構いません。

Visual Studio Code をインストールします

https://code.visualstudio.com/
をブラウザーで開き、ダウンロードして、実行します。

インストール オプションはすべてデフォルトで構いません。

インストールが完了したら、Visual Studio Code を起動して
タスクバーにピン止めすることをオススメします。

拡張機能をインストールします

Visuatl Studio Code の拡張機能を次の手順でインストールします。

  1. Visuatl Studio Code を開きます(タスクバーにピン留めしたアイコンや、スタート メニューから開きます)
  2. Visuatl Studio Code の拡張機能のボタンを押し、検索ボックスをクリックします
  3. 検索ボックスを使って、以下の拡張機能をインストールします
    • Playwright Test for VSCode

公式ドキュメントで「拡張機能」といえば Visual Studio Code エクステンションのことです。

インストールしたら、Visual Studio Code を閉じます。

Playwright のプロジェクトを新規作成します

Node.js 18 または 16 を使う場合(通常)

PowerShell や Git bash を開き、プロジェクト フォルダーを新規作成して、そこに移動します。以下のコマンドを入力するか、エクスプローラーで新規作成してそのフォルダーを cd と入力したシェルにドラッグ&ドロップします。例:デスクトップ/try_playwright

mkdir  $HOME\Desktop\try_playwright
cd  $HOME\Desktop\try_playwright

Playwright のプロジェクトを新規作成します。以下のコマンドを入力します。

npm init -y playwright  #// オプションはすべてデフォルト。Enter を何度も押します。

(以降に続きます)

Node.js 14 を使う場合

シェルで、プロジェクト フォルダー を新規作成する場所のフォルダーに移動して、既存の playwright フォルダーが存在しないことを確認します。以下のコマンドを入力するか、エクスプローラーでフォルダーを cd と入力したシェルにドラッグ&ドロップします。例:デスクトップ/try_playwright を新規作成する場合、デスクトップに移動して、playwright フォルダーが存在しないことを確認します。

cd  ~/Desktop
ls  playwright    #// 存在しないことを確認します

Playwright のプロジェクトを新規作成します。以下のコマンドを入力します。

npm init -y playwright  #// オプションはすべてデフォルト。Enter を何度も押します。

PowerShell を閉じて、新しい PowerShell を開き、新しくできたフォルダーの名前を try_playwright に変え、そのフォルダーに移動します。

cd  ~/Desktop
mv  playwright  try_playwright
cd  try_playwright

(以降に続きます)

共通の続き

JavaScript の __dirname などが Visual Studio Code から警告されないように npm の @types/node を開発時だけ使うようにインストールします。

npm install --save-dev @types/node

Visual Studio Code を開きます。下記の "." の代わりに . でも構いません。

code  "."

新しく作ったフォルダーが悪意のあるフォルダーでないか警告されますが、信頼する(trust)を選びます。

Testing ビューを表示して(フラスコ ボタン を押して)、tests を開いて(tests の左の三角を押して)example.spec.ts を開きます(選びます)。もし、Windows Defender の警告が表示されたら許可してください。

example.spec.ts を実行します。Run Test ボタン を押します。

(参考)既存の Playwright のプロジェクトをインストールします

git clone するなどして Playwright のプロジェクトをインストールした場合、Playwright を起動する前に node_modules フォルダーを復帰させる必要があります。

シェルに以下のコマンドを入力します。

Project/package-lock.json ファイルがある場合:

cd __Project__
npm ci
code  "."

Project/yarn.lock ファイルがある場合:

cd __Project__
yarn install --frozen-lockfile
code  "."

この場合、Playwright のバージョンは package.json に書かれている Playwright のバージョンになります。

Playwright の動作確認をします

Playwright のプロジェクトを新規作成すると、テストコードのサンプルも作成されるので、それを動かしてみましょう。

複数のテストを実行します

Testing ビューを表示して(左下のフラスコ ボタン を押して)、tests を開いて(tests の左の三角を押して)example.spec.ts を実行します。 テストに成功したら緑色のチェックが表示されます。

1つのテストをデバッグ実行します

ステップ実行して、変数の値を参照(インスペクト)できます

Testing ビュー の example.spec.ts にマウスを合わせ、Go to Test ボタン を押して テスト コード を表示します。

デバッグしようとするテスト(has title のテスト)の関数の中にブレークポイントを張ります。行番号の左をクリックします。

デバッグしようとするテスト(has title のテスト)の Debug Test ボタン を押すと、ブラウザーが開いてブレークします。

テスト関数の左のアイコンを右クリックしても Debug Test を選ぶことができます。

初めて動かしたときは Windows Defender の警告が表示されるので許可してください。

page.goto メソッドをステップ実行する(F10キーを押す)と、指定されている URL https://playwright.dev/ が開きます。

自動的に Run and Debug ビュー(左)が表示され、変数の値やコールスタックを確認することもできます。Visual Studio Code なので、オブジェクトの中をインスペクトすることもできます。

テストを削除します

次の章ではテストを 1から作っていくので、今までに作られたテストを削除します。

  • テストを実行中なら中止します(赤い四角を押します)
  • サンプル テスト tests/example.spec.ts を削除します
  • サンプル テスト が入っている、Project/tests-examples フォルダーを削除します

基本的な自動テストを作る

以下の基本的なテストコードを作ってみましょう。

  • 入力項目にテストデータを入力する
  • ボタンを押す(入力した英文字を小文字に変換した出力値を表示します)
  • 出力値が正しいことをチェックする

テスト対象となる HTML ファイルを以下のように作成します。新しいファイルは Visual Studio Code の Explorer ビューから test フォルダーを右クリックして New File を選びます。ファイルの内容を入力したら、File >> Save メニューを選びます。

Project/test/test_target_1.html

<!DOCTYPE HTML><html><head>
<meta charset="UTF-8">
<title>Playwright のサンプル 1</title>
</head>
<body>

    <input type="text" id="input-text"/>
    <input type="button" id="input-button" value="入力" onclick="onButton()"/>
    <div id="result">未入力</div>

    <script>
    function onButton() {
        document.getElementById('result').textContent =
            document.getElementById('input-text').value.toLowerCase();
    }
    </script>
</body>
</html>

最初のテストコードを新しく作ります。

Project/test/1.spec.ts

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

test('Input test', async ({ page }) => {
    await page.goto(`${__dirname}/test_target_1.html`);

    await page.locator('#input-text').fill('ABC');  // ABC と入力する。すぐには反映されません
    await page.locator('#input-button').click();  // 出力をチェックする。すぐには反映されません
    await expect(page.locator('#result')).toHaveText('abc');  // 出力をチェックする
    await expect(page.locator('#input-text')).toHaveValue('ABC');  // input タグの場合
});

テストコードは通常の JavaScript と違って見えますが、test は分岐やループではなく、すぐに () => {...} の部分を呼び出す関数の呼び出しであるため、通常の JavaScript と同様に上から下に実行されます。詳細は下記で説明します。

テストを実行する、デバッグ実行する、ページのサイズを変える

Visual Studio Code を開いている場合、Testing ビューの一般的な使い方でテストを実行できます。「Playwright の動作確認をします」の章を参照してください。

また、シェルからテストを実行することもできます。

npx playwright test

インストールしていないブラウザーについてはテストが失敗します。playwright.config.ts ファイルの projects からテスト対象外にするブラウザーを コメント アウト します。

Project/playwright.config.ts

...
projects: [
    {
    name: 'chromium',
    use: { ...devices['Desktop Chrome'] },
    },

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

それぞれのテストが終わったときにスクリーンショットを撮るには、playwright.config.tsscreenshot: 'on', を追記します。

export default defineConfig({
    use: {
        screenshot: 'on',

(補足)ページのサイズを調整するには、playwright.config.ts ファイル に viewport を追記するらしいのですが、うまくいきませんでした。

Project/playwright.config.ts

export default defineConfig({
    use: {
        viewport: { width: 480, height: 320 },

現在処理中の行がどこにあるかが Visual Studio Code に表示されます。

非同期の処理に await を指定しなかった場合、現在の行が複数の場所に表示されます。 非同期処理を追うことは非常に難しいのでこの機能はとても助かります。

ID を間違えたり expect の条件を満たさないとテストは待ちの状態に入り、コードの行末に ⌛waiting… と表示されます。

  • Run Test(デバッガーなし)で locator を実行中の場合、30秒後にタイムアウトのエラーになります。実行を中断するにはブラウザーを閉じます
  • Debug Test(デバッガーあり)で locator を実行中の場合、タイムアウトになることはありません。実行を中断するにはデバッガーを終了します
  • expect を実行中の場合、Run Test でも Debug Test でも 5秒でタイムアウトのエラーになります。

Web アプリケーションにデバッガーを使う

Web アプリケーションにデバッガーを使うには、ブラウザーを開き続けるようにします。

設定 Reuse Browser にチェックを入れます。

  • VSCode >> 歯車 ボタン(左下)>> Settings >> Playwright: Reuse Browser(と入力)
    ちなみに Playwright: だけ入力すると Playwright に関する設定を一覧できます

テストを実行します:

  • VSCode >> Testing(フラスコ ボタン:左)>>(テストにマウスを合わせて)Run Test(デバッガーなし)または Debug Test(デバッガーあり)
  • Debug Test で起動するデバッガーはテストに対してのみデバッグできます

ブラウザーが開いたら、Web アプリケーション にブレークポイントを張ります:

  • Chromium ブラウザーをクリックしてから F12 キー を押してブラウザーの開発者ツールを開いて、ブレークポイントを張ります

テストを再実行します。ブラウザーがブレークします。Visual Studio Code(テスト)はクリックが完了するまで待ち状態になります。

デバッグが終わったら:

  • 設定 Reuse Browser のチェックを外します

チェックを外すと Run Test(デバッガーなし)で実行するときにブラウザーが開かないようになり、速くなります。
また、Debug Test(デバッガーあり)で実行してテストが終わるとブラウザーが閉じるようになります。

page.goto - ページを開く

このサンプルでは、Web サーバーを使っていません。その場合、HTML ファイルがあるパスを、テスト ファイル があるフォルダーからの相対パスで指定します。

Project/tests/1.spec.ts:

test('My First Test', async ({ page }) => {
    await page.goto(`${__dirname}/test_target_1.html`);

Web サーバーに対してテストをするときは、URL を指定します。

    await page.goto('https://example.com/');

baseURLplaywright.config.ts ファイルに設定することができます。Playwright プロジェクトを新規作成したときの内容から以下のように編集します。

playwright.config.ts:

export default defineConfig({
    use: {
        baseURL: 'http://127.0.0.1:3000',

baseURL を設定した場合、http://127.0.0.1:3000 の右側を指定します。

await page.goto('/test_target_1.html');
    // http://127.0.0.1:3000/test_target_1.html を開きます

locator, get - GUI部品を選択する

操作やチェックをする対象となる GUI部品を特定するときは、locator メソッドや get から始まるメソッドを指定します。 cypress の Selector Playground のような機能はありませんが、ブラウザーの開発者ツールを使えば同様のことができます。

ただし、Selector Playground を使う前に HTML に id 属性または data- 属性(例:data-test 属性)を記述しておきます。id 属性等は必須ではありませんが id 属性等が記述してあると、シンプルで変化に強いコードになります。

<input type="text" id="input-text"/>

id 属性を指定する場合、テストは以下のように書きます。パラメーターは CSS セレクター です。

await page.locator('#input-text').fill('ABC');

locator 以外に $, getByTestId, getByLabel, getByPlaceholder, getByText, getByRole などがありますが、await が必要なので若干コードが複雑になってしまいます。

await (await page.$('#input-text'))?.fill('ABC');

また、locator メソッドにマウスを合わせると、Reuse Browser にチェックを入れて開いたブラウザーの中の該当する GUI 部品が強調表示され、GUI 部品の位置がすぐに分かります。 $, get メソッドでは表示されません。

name 属性を指定する場合、テストは以下のように書きます。

<input type="text" name="input-text"/>

await page.locator('[name=input-text]').fill('ABC');

data-test 属性を指定する場合、テストは以下のように書きます。

<input type="text" name="input-text" data-test="input-text"/>

await page.locator('[data-test=input-text]').fill('ABC');

Chromium では次の手順で cypress の Selector Playground と同様のことができます。

  • テストを開始してブラウザーを開きます
  • F12 キー を押して Element タブを選びます
  • Select an element in the page to inspect it(Element タブの左)を押して GUI 部品をクリックします
  • 選択された HTML 要素を右クリック >> Copy >> Copy selector
  • テストコードに貼り付ける

貼り付けられるセレクターは以下のようになります。

#input-text

CSS を使うときの CSS セレクターは、class 属性を指定することが多いですが、Playwright などのテストコードには class 属性を指定しないでください。

  • id 属性または name 属性または data- 属性を指定してください。なぜなら CSS の都合で class 属性の値が編集されたときに locator が失敗してしまうからです。

    // await page.locator('.input-text') class 属性の指定は禁止

  • id 属性が対象となる GUI 部品を表す ID として適切ではない値がすでに記述されている場合や、id 属性が重複していている場合など、後で修正する可能性がある場合は、data- 属性を cy.get に渡すようにすると、id 属性が修正されたときの影響を受けなくて済むようになります。

GUI部品を絞り込む

<table id="table1"><tr><td>
    <input type="text" id="input-text"/>
</td></tr></table>

<table id="table2"><tr><td>
    <input type="text" id="input-text"/>
</td></tr></table>

同じ id 属性が複数の HTML 要素に設定してある場合、ブラウザーの Copy selector 機能でコピーすると複数の HTML 要素にマッチするセレクターがコピーされます。

#input-text

とりあえず テスト コード に貼り付けると次のようになります。

await page.locator('#input-text').fill('ABC');

locator メソッドのコードにマウスを合わせると、Reuse Browser にチェックを入れて開いたブラウザーの中で複数の GUI 部品が強調表示されるのが分かります。

table2 の中の input-text は次のように書きます。

await page.locator('#table2 #input-text').fill('ABC');

2つ目にマッチする GUI 部品を特定するには .nth(1) を使います。

await page.locator('#input-text').nth(1).fill('ABC');

下記のようなセレクターでも location メソッドに指定することができます。

await page.locator('#table2 > tbody > tr > td > #input-text').fill('ABC');

ただし、このようなセレクターを Chromium ブラウザーの Copy selector 機能でコピーするには、一時的に id 属性を無効(id___など適当な名前)にしてからコピーする必要があります。

locator メソッドの第2引数だけ使う絞り込みを行うときは、filter メソッドを使います。

expect, toHaveText, toHaveValue - 値をチェックする

テストをすることは、出力値が正しい値(期待する値)であることをチェックすることです。

HTML タグの間のテキストをチェックするテストコードと、input タグの中のテキストをチェックするテストコードは、以下のように若干異なるので注意してください。

HTML タグの間のテキストをチェックする

await expect(page.locator('#result')).toHaveText('abc');

input タグの中のテキストをチェックする

await expect(page.locator('#input-text')).toHaveValue('ABC');

よく使われる GUI 部品の自動テストを作る

以下のよく使われる GUI 部品のテストコードを作ってみましょう。

  • チェックボックスを操作する
  • チェックボックスのチェック状態をチェックする
  • ドロップダウンリストを操作する
  • ドロップダウンリストの選択状態をチェックする
  • 表の中のテキストをチェックする

テスト対象となる HTML ファイルを以下のように作成します。

Project/test/test_target_2.html

<!DOCTYPE HTML><html><head>
<meta charset="UTF-8">
<title>Playwright のサンプル 2</title>
</head>
<body>

    <input type="checkbox" id="input-check"/>チェック

    <select id="input-drop-down">
        <option value="a">選択肢1</option>
        <option value="b">選択肢2</option>
        <option value="c" selected>選択肢3</option>
    </select>

    <table id="result-table" style="border-style: solid">
        <tr><td id="table-id-0">A</td><td id="table-value-0">100</td></tr>
        <tr><td id="table-id-1">B</td><td id="table-value-1">200</td></tr>
        <tr><td id="table-id-2">C</td><td id="table-value-2">300</td></tr>
    </table>

</body>
</html>

テストコードは以下のようになります。

Project/test/2.spec.ts

import { test, expect, Page } from '@playwright/test';
import { getRowIndex } from './lib';

let  page:Page;

test.beforeAll(async ({ browser }) => {
    // ページをここで作ります。
    // 各テストを実行する前に blank ページに戻らなくなります。
    // できれば、各テストを実行する前に blank ページに戻し、beforeEach で page.goto してください。
    page = await browser.newPage();

    await page.goto(`${__dirname}/test_target_2.html`);
});

test.beforeEach(async () => {
});

test('check box test', async () => {
    await page.locator('#input-check').click()
    await expect(page.locator('#input-check')).toBeChecked();  // .not.toBeChecked
});

test('drop down list test', async () => {
    await page.locator('#input-drop-down').selectOption('b');
    await expect(page.locator('#input-drop-down')).toHaveValue('b');
});

test('Table test', async () => {
    const  td = page.locator('#result-table  td:has-text("B")');
    const  iRow = await getRowIndex(page, td);
    await expect(page.locator(`#table-value-${iRow}`)).toHaveText('200');
});

test.afterEach(async () => {
});

test.afterAll(async () => {
    await page.close();
});

Project/test/lib.ts

import { Page, Locator } from '@playwright/test';

export async function  getRowIndex(page: Page, td: Locator): Promise<number> {
    const  attributeName = 'data-index';  // HTML element data- attribute name reserved by this function
    const  trs = (await td.locator('xpath=../..').locator('tr').elementHandles())!;  // ../../tr HTML tags
    await page.evaluate(({trs, attributeName}: {trs: any, attributeName: string}) => {
        for (let i = 0; i < trs.length; i+=1) {

            trs[i].setAttribute(attributeName, i);
        }
    }, {trs, attributeName});
    const  tr = (await td.locator('xpath=..').elementHandle())!;  // A parent of td HTML tag

    const  attribute = await tr.getAttribute(attributeName);
    if (!attribute) {
        return  -1;
    }

    return  parseInt(attribute);
}

describe, test, beforeAll, beforeEach, afterEach, afterAll - テストコードの構成

テストコードは通常の TypeScript と違って見えますが、 test.describe, test は分岐やループではなく、すぐに () => {...} の部分を呼び出す関数の呼び出しであるため、 通常の TypeScript と同様に上から下に実行されます。

厳密には、() => { ... } の中の部分は非同期に実行され、
() => { ... } の外の部分が先に実行されるのですが、
() => { ... } の中だけ見れば上から下に実行します。

定数データの定義やループは、() => { ... } の外に記述することもできます。ループの中の () => { ... } の中の部分も繰り返し実行されます。

test.describe はテストケースのグループに相当します。

test.beforeEach() => { ... } の中は、それぞれの it の () => { ... } の中を実行する直前に実行します。
test.afterEach() => { ... } の中は、それぞれの it の () => { ... } の中を実行した直後に実行します。
test.beforeAll() => { ... } の中は、最初の test.beforeEach() => { ... } の中を実行する直前に実行します。
test.afterAll() => { ... } の中は、最初の test.afterEach() => { ... } の中を実行する直前に実行します。

つまり、test が3つある場合は、下記の順番で実行します。

  • test.beforeAll
  • test.beforeEach
  • test
  • test.afterEach
  • test.beforeEach
  • test
  • test.afterEach
  • test.beforeEach
  • test
  • test.afterEach
  • test.afterAll

test.describe はネストすることもできます。ネストするとテストのレポートがネストして表示されますが、ネストに関係なく通常の JavaScript と同様に上から下に実行します。

test.describetest.only を付けると、その部分だけ実行します。ただし、その前後の test.beforeAll, test.beforeEach, test.afterEach, test.afterAll も実行します。

test.only('check box test', async () => {

test.describetest.skip を付けると、その部分だけ実行しません。

test.skip('check box test', async () => {

test の関数が呼ばれるたびに、ブラウザーが blank ページに移動した状態になり、テストのたびに page.goto をしなければなりませんが、上記のサンプルは、1つの テスト ファイル のテストを実行している間はリセットしないようにしています。

しかし、なるべくそれぞれのテストのたびに blank ページに移動した状態から始めるようにしたほうがテストの品質が高まります。 セッションを再利用する方法はここでは説明しませんが方法はあります。

表の中のテキストをチェックする

行の順番が重要ではない表形式で出力されるとき、行が入れ替わって出力されることがあります。

そのテストは、チェック対象の行を検索してから、見つかった行の中の項目の内容をチェックするとよいでしょう。下記のコードは、B という項目がある行を検索し、行番号のマイナス1が iRow 引数に渡され、iRow を使った id 属性の項目のテキストをチェックしています。B という項目は 2行目にあるので iRow は 1 になります。id="table-value-1" の項目のテキストが 200 ならテストは成功します。なお、getRowIndex 関数は Playwright が提供している関数ではなく独自に作成した関数です。

const  td = page.locator('#result-table  td:has-text("B")');
const  iRow = await getRowIndex(page, td);
await expect(page.locator(`#table-value-${iRow}`)).toHaveText('200');

その他よく使われるメソッド

href 属性が /users であることをチェックします

await expect(page.locator('a')).toHaveAttribute('href', '/users');

src 属性が指定の文字列を含むことをチェックします(画像のファイル名)

expect(await page.locator('img').getAttribute('src')).toContain('partOfString')

src 属性が指定の正規表現にマッチすることをチェックします

expect(await page.locator('img').getAttribute('src')).toMatch(/regularExpression/);

CSS display がすでに none になっていることをチェックします

expect(await page.locator('#element').evaluate((e) => getComputedStyle(e).display)).toBe('none');

CSS display による非表示を待ちます

await page.waitForFunction(() => getComputedStyle(document.querySelector('#element')!).display === 'none');

class 属性に指定できる複数の値のうち、1つでも disbled があることをチェックします

expect(await page.locator('#element').getAttribute('class').split(' ')).toContain('disabled');

表示されるまで待ちます。デフォルトでは 30秒でタイムアウトします。

await page.waitForSelector('#element');

フォーカスされるまで待ちます

await page.waitForFunction(sel => document.activeElement === document.querySelector(sel),{}, '#element');

手動テスト

自動化することが難しい一部の操作に関しては、手動で操作したり目視でチェックしたりするとよいでしょう。

debugger;

テストの中で debugger を呼び出すと、Visual Studio Code がブレークポイントで止まったときと同じ動きをします。なお await page.pause(); は CLI からテストを実行したときに使います。

手動テストで操作する内容をコメントに書いておくとよいでしょう。

if (process.env.MANUAL_TEST) {
    // 手動で~の操作をしたら、続行してください。参考:エクセルの No.5 シート
    debugger;
}

環境変数を定義するには、Visual Studio Code を起動するときなどに環境変数を設定します。

bash

MANUAL_TEST=1  code "__Project__"

止まってしまうとき

テストコードに対してブラウザーは非同期で動作するため、対象の GUI 部品をうまく捕捉できないで止まることや操作やチェックができないことがあります。そのあたりのノウハウについては、cypress ですが別の記事で説明しています。

cypress で期待通りに自動テストが動かなかったときの対策集

E2Eテストを自動化して早期にバグを無くす

Playwright を使うことで Web アプリケーションの E2Eテストを簡単に自動化することができます。

自動テストを行うと早期にバグが見つかり、開発中のデグレードが発生する可能性が下がり、開発をスムーズに進めることができるようになります。Web アプリケーションの場合、結合テストは、E2E(End to End)Test とも呼ばれ、エンド ユーザーが行う操作と同じようなテストケースを動かして正しい結果が出力されることをチェックするテストです。 それに対して、ユニットテストは、プログラミング言語(関数やクラス)に対してテストケースを動かして正しい結果が出力されることをチェックするテストです。

ユニットテストだけしている人は多いと思いますが、テスト済みのユニットを集めたシステムでもバグが発生します。ユニット以外の結合部分のコードの量は同じぐらいあるので同じぐらいバグが発生する可能性があります。結合するだけだからバグは存在しないと考えているなら間違いです。開発終盤にライブラリやフレームワークをバージョンアップしないですよね。

結合テストをすることで、エンドユーザーにとって基本的な動作が正しいことを実証できます。 ユニットテストはいくら厳密にしても細かい部分しか実証されません。むしろ、結合テストをすれば、エンドユーザーの視点で細かい部分のバグしか残らないので、結合テストを優先すべきとも考えられます。

自動テストを作るのは難しくない

自動テストのコードを書くのは難しいと思われるかもしれませんが、ほとんどのコードはコピペするだけで作ることができます。Web アプリケーションはゲームと異なり、項目を入力してボタンを押すという単純な操作が大半を占めるので、いくつかのパターンをコピペして使えるようになればほとんどのテストケースを自動化することができるようになります。

コードレスで自動テストが作れるツールもありますが、Chromium には Selector copy 機能があります。操作やチェックをしたい GUI 部品(ウィジェット)をクリックするだけでGUI 部品を表すコードをコピペできます。後は、GUI 部品を表すコードに、操作やチェックするメソッドを、サンプルコードからコピペしてパラメーターを編集するだけです。コードレスと異なり、テストコードは git でバージョン管理もできます。コードの言語は、TypeScript です。インテリセンスで使えるメソッドがいちらんできたり、型を使った静的解析によって実行する前に問題点が早期に明らかになります。

Playwright を使って自動テストを作れるようになったスキルは、今後も活用できるでしょう。なぜなら Playwright は HTML に対して自動化するため、フロントエンドのフレームワークやプログラミング言語が変わっても HTML 部分は変わないからです。

細かい部分まで完全に自動化しようとすると大変ですが、難しい部分は手動で操作するようにすることもできます。完全自動化することは楽しくクールですが、割り切ることも大事です。

また、テストは網羅的である必要があると考えがちですが、ほとんどのコードは2つのテストデータだけ通れば、ほとんどのバグはなくなります。境界値テストは境界を超える可能性が高いケースだけで十分です。テストは入力値を与えて動作させ、出力値が正しい値と合っているかをチェックするわけですが、バグがあるのに出力値と正しい値が等しくなる可能性は非常に少ないです。網羅的にチェックする必要があるロジックについて関数やクラスに抽出し、それに対して網羅的なユニットテストをするとよいでしょう。ライブラリでテスト済みのケースも自分がテストする必要はありません。

参考

Discussion