Playwrightを用いたDjangoアプリのEnd-to-Endテスト
この記事の目次
はじめに
Playwrightを用いて、以前作成した日記アプリに対してEnd-to-Endテストを実施します。日記アプリについてはこの記事の冒頭を参照してください。
End-to-Endテストの概要
End-to-Endテストはシステム全体に対して行われるテストです。
そのシステムの利用者の目線に立って、ブラウザ上で入力してから実行結果が返ってくるまでの一連の流れを検証します。
次の図は上から順にEnd-to-Endテスト、インテグレーションテスト、ユニットテストを表しています。
上に行くほど、テストの数は少なくなり、テストの対象範囲は広くなります。
Playwrightの紹介
PlaywrightはEnd-to-Endテストを行うソフトウェアの一種で、Microsoft社によって開発されています。
Playwrightには次の特徴があります。
- 要素が実行可能になるまで自動的に待つことで、テストが不安定になることを防ぐ
同一の条件で同じテストを複数回実施した際に、失敗する場合と成功する場合があるという不安定な状態になってしまうことがあります。
このような状態のテストはFlaky testとよばれています。
Seleniumなどのソフトウェアを使用してテストを行う場合には、「システムの応答が完了するまで一定時間時間待つ」といったコードを開発者が書く必要があります。
しかし、何らかの原因でシステムの応答が完了するまでに通常より長く時間がかかってしまう場合があります。
この場合には、実行可能になる前に要素を参照しようとすることでエラーが起き、テストが失敗してしまうといったことがあります。
このように、システムの応答が完了するまでの待機時間を開発者側で指定することは、Flaky testの原因になりえます。
Playwrightでは、要素が実行可能になるまで自動的に待機することで、上に述べたような事態によってFlaky testが発生してしまうのを防ぐことができます。
Playwrightのインストール
こちらのページを参考にしてください
私はnpmを使用してPlaywrightをインストールしました。事前にバージョン条件を満たしたnode.jsがインストールされている必要があることに注意して下さい。
インストール時に指定したテストフォルダにはexample.spec.tsファイル(Typescriptを選択した場合)が置いてあります。
このファイルにはhttps://playwright.dev/ (PlayWrightの公式ページ)に対する3つのテストが存在します。
ターミナル上でnpx playwright test
と入力すると、テストフォルダ内のテストコードがすべて実行されます。
テストが完了すると、次のようにテスト結果がブラウザ上に表示されます。
上の画面では「has title」という名前のテストが「chromium」、「firefox」、「webkit」の3つのブラウザで実行されて、成功したことを表しています。
PlaywrightによるE2Eテストの記述方法
Playwrightでは、~.spec.ts(Javascriptの場合はspec.js)という名前のファイルの中にテストを書いていきます。
ファイル内では、
import { test, expect } from '@playwright/test';
test('テスト名', async ({ page }) => {
});
await page.getByRole('button', { name: 'Sign in' }).click();
VSCodeの拡張機能を使用する
PlayWrightの拡張機能である「Playwright Test for VSCode」を使用することで、容易にテストの生成やテスト範囲の制御を行うことができます。
テストの生成
拡張機能のインストールが完了したら、サイドバーでこちらのアイコン
をクリックして、拡張機能のメニュー画面を開いてください
メニュー画面の下部を見てください。
テストで使用するブラウザを、「PROJECTS」で選択できます。
「SETTING」では、テスト実行時のオプションが選択できます。
「Show browser」では、テスト実行時にブラウザが立ち上がって、テストの動作をリアルタイムで確認することができます。
「Show trace viewer」では、テスト終了時にブラウザ上で下の画像のような詳細な記録を見ることができます。
「Record new」ボタンを押すと、次のようにtest-1.spec.tsというファイルが生成されるとともに、ブラウザが開きます。
ブラウザでテストを行いたいページにアクセスして、テストしたい箇所の操作を行うことで、test-1.spec.ts上にテストコードが生成されていきます。
例えば、「トップページからログイン画面にアクセスして、ログイン画面でメールアドレスとパスワードを入力してログインする」という操作をテストしたい場合を考えます。この場合は、トップページからログイン画面にアクセスして、ブラウザ上でメールアドレスとパスワードを入力してログインボタンを押します。すると、次のようにテストコードが生成されています。
後は、実際にテストを実行してみて、過不足がある部分を手作業で修正することでテストコードが完成します。
「Playwright Test for VSCode」に関する詳しい説明はこちらのページを参考にしてください。
テスト範囲の制御
先ほどのように、
npx playwright test
を実行すると、テストフォルダ内のテストすべてが実行されます。「Playwright Test for VSCode」を使うことで、テストの実行範囲をファイル単位や個別のテスト単位で細かく制御することができます。
今回使用したテストは次のような構造になっています。例えば、login.spec.ts内にはloginとsignupという名前の
2つのテストが存在します。
各ファイルやテスト名の横の「Run Test」ボタン(▷のアイコン)を押すことでテストの実行ができます。
login.spec.tsの横の「Run Test」ボタンを押すとファイル内のlogin, signupテストが実行されます。
loginの横の「Run Test」ボタンを押すと、loginテストのみが実行されます。
日記アプリに対するEnd-to-Endテストの実行
Playwrightを用いて、実際に日記アプリに対してEnd-to-Endテストを実行します。
実施したそれぞれのテストに関して、テストの概要や使用したコード、スクリーンショットを記載しています。(page.screenshot()によって、現在テストを行っている画面のスクリーンショットが取得できます。)
アカウントの新規作成
まず、アカウントの新規作成ができるか確認します。
– 実装
test('signup', async ({ page }) => {
await page.goto('http://localhost:8000/'); //トップページを開く
await page.getByText('SIGN UP').click(); //サインアップページへ
//メールアドレスとパスワードを入力して新規登録
await page.getByPlaceholder('Email address').fill('new@gmail.com');
await page.getByPlaceholder('Email address').press('Tab');
await page.getByText('Password:').fill('00001111aa');
await page.getByText('Password:').press('Tab');
await page.getByText('Password (again):').fill('00001111aa');
await page.screenshot({ path: 'signup_diary_1.png' });
await page.getByRole('button', { name: '登録' }).click();
await page.screenshot({ path: 'signup_diary_2.png' });
});
ログイン、ログアウト
先ほど作成したアカウントでログインをした後にログアウトします。
test('login_logout', async ({ page }) => {
await page.goto('http://localhost:8000/'); //トップページを開く
await page.getByRole('link', { name: 'LOG IN', exact: true }).click(); //ログインページへ
//メールアドレスとパスワードを入力してログイン
await page.getByPlaceholder('Email address').fill('new@gmail.com');
await page.getByPlaceholder('Email address').press('Tab');
await page.getByPlaceholder('Password').fill('00001111aa');
await page.getByLabel('Remember Me:').check();
await page.getByRole('button', { name: 'ログイン' }).click();
await page.screenshot({ path: 'login_diary.png' });
//ログアウト
await page.getByText('LOG OUT').click();
await page.screenshot({ path: 'logout_diary.png' });
});
日記の新規作成
Playwrightを用いて、新しい日記を作成します。
日記のタイトルを「Playwrightテスト」、本文を「Playwrightテスト本文」として、画像を1枚挿入しています。
//日記の新規作成のテスト
test('create', async ({ page }) => {
await page.goto('http://localhost:8000/accounts/login/');
await page.getByPlaceholder('Email address').click();
await page.getByPlaceholder('Email address').fill('new@gmail.com');
await page.getByPlaceholder('Email address').press('Tab');
await page.getByPlaceholder('Password').fill('00001111aa');
await page.getByRole('button', { name: 'ログイン' }).click();
await page.getByRole('link', { name: '新規作成' }).click();
await page.getByLabel('タイトル:').click();
await page.getByLabel('タイトル:').fill('Playwrightテスト');
await page.getByLabel('タイトル:').press('Tab');
await page.getByLabel('本文:').fill('Playwrightテスト本文');
await page.getByLabel('写真1:').click();
await page.getByLabel('写真1:').setInputFiles('/home/yusuke/record/image-102.png'); //てきとうな写真をアップロード
await page.screenshot({ path: 'create_diary_1.png' });
await page.getByRole('button', { name: '作成' }).click();
await page.screenshot({ path: 'create_diary_2.png' });
});
スクリーンショットを確認すると、無事に日記が作成されていることが確認できます。
日記の編集
先ほど作成した日記の編集を行います。
新たに画像を1枚挿入して、ブログの本文を「Playwrightテスト本文更新」とします。
test('update', async ({ page }) => {
await page.goto('http://localhost:8000/accounts/login/');
await page.getByPlaceholder('Email address').click();
await page.getByPlaceholder('Email address').fill('new@gmail.com');
await page.getByPlaceholder('Email address').press('Tab');
await page.getByPlaceholder('Password').fill('00001111aa');
await page.getByLabel('Remember Me:').check();
await page.getByRole('button', { name: 'ログイン' }).click();
await page.getByRole('link', { name: 'Playwrightテスト Playwrightテスト本文' }).first().click();
await page.getByRole('link', { name: '編集' }).click();
await page.getByLabel('本文:').click();
await page.getByLabel('本文:').fill('Playwrightテスト本文更新');
await page.getByLabel('写真2:').click();
await page.getByLabel('写真2:').setInputFiles('/home/yusuke/record/image-3.png');
await page.screenshot({ path: 'update_diary_1.png' });
await page.getByRole('button', { name: '更新' }).click();
await page.screenshot({ path: 'update_diary_2.png' });
});
日記の削除
作成した日記を削除します。
test('delete', async ({ page }) => {
await page.goto('http://localhost:8000/accounts/login/');
await page.getByPlaceholder('Email address').click();
await page.getByPlaceholder('Email address').fill('new@gmail.com');
await page.getByPlaceholder('Password').click();
await page.getByPlaceholder('Password').fill('00001111aa');
await page.getByRole('button', { name: 'ログイン' }).click();
await page.getByRole('link', { name: 'Playwrightテスト Playwrightテスト本文更新' }).click();
await page.getByRole('link', { name: '削除' }).click();
await page.screenshot({ path: 'delete_diary_1.png' });
await page.getByRole('button', { name: '削除' }).click();
await page.screenshot({ path: 'delete_diary_2.png' });
});
まとめ
以上のように、Playwrightを使用することで以下の操作が正しく行われることを確認しました。
- アカウントの作成
- ログイン、ログアウト
- ブログ記事の新規作成
- ブログ記事の編集
- ブログ記事の削除