はじめに
皆さん画面のテストってどうやってますか?
ブラウザ立ち上げてポチポチやってますか?
もちろん人間による手動確認も必要ですが、毎回やるのは手間がかかりすぎて大変です。
そこで今回は「Playwright」というコードで記述をしてUIテストを自動化できるライブラリを触ってみたいと思います。
検証をした環境や技術
| 項目 | 名称やバージョン等 |
|---|---|
| ホストマシン | Windows 11 Pro |
| 動作環境 | Ubuntu 24.04(WSL2) |
| Node | 22.23.1 |
| npm | 10.9.8 |
| Playwright | 1.62.0 |
| プログラミング言語 | TypeScript |
1. Playwrightをインストールする
こちらの公式サイトを見ながら実施します。
今回の作業用のプロジェクトを作成します。
mkdir hello-playwright
cd hello-playwright
以下のコマンドでPlaywrightをインストールする。
npm init playwright@latest
すると以下のような質問がされるので、必要に応じて回答をします。
今回私は以下の通り回答をしました↓
-
TypeScript or JavaScript (default: TypeScript): TypeScript -
Tests folder name (default: tests, or e2e if tests already exists): tests -
Add a GitHub Actions workflow (recommended for CI): No -
Install Playwright browsers (default: yes): Yes -
Install Playwright operating system dependencies: Yes
しばらく待つと、自動でPlaywrightを動かすためのテンプレートプロジェクトが作成されて、インストールが完了します。
これはバージョンや環境によってケースバイケースかもしれませんが、自動で作成されたplaywright.config.tsの中の「process.env.xxx」の箇所でエラーとなり、「Cannot find name 'process'. Do you need to install type definitions for node?」となり、Node.js の型定義 (@types/node)が認識されていないというエラーが発生したので、以下のようにtsconfig.jsonを手動で新規作成して、以下のように記述をすることでエラーは解消されました。
ここは必要に応じて対応が必要そうです。
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"types": ["node"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": [
"**/*.ts"
]
}
インストールに成功すると、最後にこのようなメッセージが出力されます。
✔ Success! Created a Playwright Test project at /mnt/c/dev/hello-playwright
Inside that directory, you can run several commands:
npx playwright test
Runs the end-to-end tests.
npx playwright test --ui
Starts the interactive UI mode.
npx playwright test --project=chromium
Runs the tests only on Desktop Chrome.
npx playwright test example
Runs the tests in a specific file.
npx playwright test --debug
Runs the tests in debug mode.
npx playwright codegen
Auto generate tests with Codegen.
We suggest that you begin by typing:
npx playwright test
And check out the following files:
- ./tests/example.spec.ts - Example end-to-end test
- ./playwright.config.ts - Playwright Test configuration
Visit https://playwright.dev/docs/intro for more information. ✨
Happy hacking! 🎭
これでPlaywrightのインストールは完了です。
2. Playwrightを動かしてみる
自動で作成されたtests\example.spec.tsの中を見てみると以下のような実装がされてます。
import { test, expect } from '@playwright/test';
// テスト1
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/Playwright/);
});
// テスト2
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// Click the get started link.
await page.getByRole('link', { name: 'Get started' }).click();
// Expects page to have a heading with the name of Installation.
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
2つのテストが実装されています。
2-1. テスト1. タイトルの確認
Playwrightの公式サイト「https://playwright.dev/」にアクセスし、「タイトルの中に『Playwright』という文字列が含まれてること」を確認するテストをしています。
2-2. テスト2. リンク先の見出し確認
Playwrightの公式サイト「https://playwright.dev/」にアクセスし、「Get started」のリンクをクリックし、そのリンク先の見出しが「Installation」であることを確認するテストをしています。
2-3. テストを実行してみる
以下のコマンドを実行します。
npx playwright test
すると以下のような出力がされて、テストは完了となります。
Running 6 tests using 6 workers
6 passed (7.2s)
To open last HTML report run:
npx playwright show-report
playwright-report\index.htmlというテストレポート用のHTMLが生成されるので確認します。
確かに出力の通り、6つのテストが「Passed」になっています。
これは2つのテストを3つの環境で実行をしたから合計で2 × 3 = 6つのテストが実行されたということになっています。
これはplaywright.config.tsの中の設定で変えることができます。
自動で作成されたplaywright.config.tsではこのようになっています。
// 一部抜粋
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
]
上記の通り、playwright.config.tsの中のprojectsというパラメータで「chromium」と「firefox」と「webkit」の3つが指定されているので、テストレポートの内容とも一致していますね。
対応しているブラウザやデバイスの一覧を確認しようと、Playwright公式サイトのリンクからGithubのページに飛んだのですが、まさかの「404 Page Not Found」でした。
github.com/microsoft/playwright - registry of device parameters
なので、Playwrightのソースコードを直接辿って見つけたので一応共有しておきます。
https://github.com/microsoft/playwright/blob/main/packages/playwright-client/types/types.d.ts#L26136
types.d.ts(2026年7月27日時点)
type Devices = {
"Blackberry PlayBook": DeviceDescriptor;
"Blackberry PlayBook landscape": DeviceDescriptor;
"BlackBerry Z30": DeviceDescriptor;
"BlackBerry Z30 landscape": DeviceDescriptor;
"Galaxy Note 3": DeviceDescriptor;
"Galaxy Note 3 landscape": DeviceDescriptor;
"Galaxy Note II": DeviceDescriptor;
"Galaxy Note II landscape": DeviceDescriptor;
"Galaxy S III": DeviceDescriptor;
"Galaxy S III landscape": DeviceDescriptor;
"Galaxy S5": DeviceDescriptor;
"Galaxy S5 landscape": DeviceDescriptor;
"Galaxy S8": DeviceDescriptor;
"Galaxy S8 landscape": DeviceDescriptor;
"Galaxy S9+": DeviceDescriptor;
"Galaxy S9+ landscape": DeviceDescriptor;
"Galaxy S24": DeviceDescriptor;
"Galaxy S24 landscape": DeviceDescriptor;
"Galaxy A55": DeviceDescriptor;
"Galaxy A55 landscape": DeviceDescriptor;
"Galaxy Tab S4": DeviceDescriptor;
"Galaxy Tab S4 landscape": DeviceDescriptor;
"Galaxy Tab S9": DeviceDescriptor;
"Galaxy Tab S9 landscape": DeviceDescriptor;
"Galaxy Z Fold 6": DeviceDescriptor;
"Galaxy Z Fold 6 landscape": DeviceDescriptor;
"Galaxy Z Fold 6 Cover": DeviceDescriptor;
"Galaxy Z Fold 6 Cover landscape": DeviceDescriptor;
"Galaxy Z Fold 7": DeviceDescriptor;
"Galaxy Z Fold 7 landscape": DeviceDescriptor;
"Galaxy Z Fold 7 Cover": DeviceDescriptor;
"Galaxy Z Fold 7 Cover landscape": DeviceDescriptor;
"Galaxy Z Flip 6": DeviceDescriptor;
"Galaxy Z Flip 6 landscape": DeviceDescriptor;
"Galaxy Z Flip 6 Cover": DeviceDescriptor;
"Galaxy Z Flip 6 Cover landscape": DeviceDescriptor;
"Galaxy Z Flip 7": DeviceDescriptor;
"Galaxy Z Flip 7 landscape": DeviceDescriptor;
"Galaxy Z Flip 7 Cover": DeviceDescriptor;
"Galaxy Z Flip 7 Cover landscape": DeviceDescriptor;
"iPad (gen 5)": DeviceDescriptor;
"iPad (gen 5) landscape": DeviceDescriptor;
"iPad (gen 6)": DeviceDescriptor;
"iPad (gen 6) landscape": DeviceDescriptor;
"iPad (gen 7)": DeviceDescriptor;
"iPad (gen 7) landscape": DeviceDescriptor;
"iPad (gen 11)": DeviceDescriptor;
"iPad (gen 11) landscape": DeviceDescriptor;
"iPad Mini": DeviceDescriptor;
"iPad Mini landscape": DeviceDescriptor;
"iPad Pro 11": DeviceDescriptor;
"iPad Pro 11 landscape": DeviceDescriptor;
"iPhone 6": DeviceDescriptor;
"iPhone 6 landscape": DeviceDescriptor;
"iPhone 6 Plus": DeviceDescriptor;
"iPhone 6 Plus landscape": DeviceDescriptor;
"iPhone 7": DeviceDescriptor;
"iPhone 7 landscape": DeviceDescriptor;
"iPhone 7 Plus": DeviceDescriptor;
"iPhone 7 Plus landscape": DeviceDescriptor;
"iPhone 8": DeviceDescriptor;
"iPhone 8 landscape": DeviceDescriptor;
"iPhone 8 Plus": DeviceDescriptor;
"iPhone 8 Plus landscape": DeviceDescriptor;
"iPhone SE": DeviceDescriptor;
"iPhone SE landscape": DeviceDescriptor;
"iPhone SE (3rd gen)": DeviceDescriptor;
"iPhone SE (3rd gen) landscape": DeviceDescriptor;
"iPhone X": DeviceDescriptor;
"iPhone X landscape": DeviceDescriptor;
"iPhone XR": DeviceDescriptor;
"iPhone XR landscape": DeviceDescriptor;
"iPhone 11": DeviceDescriptor;
"iPhone 11 landscape": DeviceDescriptor;
"iPhone 11 Pro": DeviceDescriptor;
"iPhone 11 Pro landscape": DeviceDescriptor;
"iPhone 11 Pro Max": DeviceDescriptor;
"iPhone 11 Pro Max landscape": DeviceDescriptor;
"iPhone 12": DeviceDescriptor;
"iPhone 12 landscape": DeviceDescriptor;
"iPhone 12 Pro": DeviceDescriptor;
"iPhone 12 Pro landscape": DeviceDescriptor;
"iPhone 12 Pro Max": DeviceDescriptor;
"iPhone 12 Pro Max landscape": DeviceDescriptor;
"iPhone 12 Mini": DeviceDescriptor;
"iPhone 12 Mini landscape": DeviceDescriptor;
"iPhone 13": DeviceDescriptor;
"iPhone 13 landscape": DeviceDescriptor;
"iPhone 13 Pro": DeviceDescriptor;
"iPhone 13 Pro landscape": DeviceDescriptor;
"iPhone 13 Pro Max": DeviceDescriptor;
"iPhone 13 Pro Max landscape": DeviceDescriptor;
"iPhone 13 Mini": DeviceDescriptor;
"iPhone 13 Mini landscape": DeviceDescriptor;
"iPhone 14": DeviceDescriptor;
"iPhone 14 landscape": DeviceDescriptor;
"iPhone 14 Plus": DeviceDescriptor;
"iPhone 14 Plus landscape": DeviceDescriptor;
"iPhone 14 Pro": DeviceDescriptor;
"iPhone 14 Pro landscape": DeviceDescriptor;
"iPhone 14 Pro Max": DeviceDescriptor;
"iPhone 14 Pro Max landscape": DeviceDescriptor;
"iPhone 15": DeviceDescriptor;
"iPhone 15 landscape": DeviceDescriptor;
"iPhone 15 Plus": DeviceDescriptor;
"iPhone 15 Plus landscape": DeviceDescriptor;
"iPhone 15 Pro": DeviceDescriptor;
"iPhone 15 Pro landscape": DeviceDescriptor;
"iPhone 15 Pro Max": DeviceDescriptor;
"iPhone 15 Pro Max landscape": DeviceDescriptor;
"iPhone 16": DeviceDescriptor;
"iPhone 16 landscape": DeviceDescriptor;
"iPhone 16 Plus": DeviceDescriptor;
"iPhone 16 Plus landscape": DeviceDescriptor;
"iPhone 16 Pro": DeviceDescriptor;
"iPhone 16 Pro landscape": DeviceDescriptor;
"iPhone 16 Pro Max": DeviceDescriptor;
"iPhone 16 Pro Max landscape": DeviceDescriptor;
"iPhone 16e": DeviceDescriptor;
"iPhone 16e landscape": DeviceDescriptor;
"iPhone 17": DeviceDescriptor;
"iPhone 17 landscape": DeviceDescriptor;
"iPhone Air": DeviceDescriptor;
"iPhone Air landscape": DeviceDescriptor;
"iPhone 17 Pro": DeviceDescriptor;
"iPhone 17 Pro landscape": DeviceDescriptor;
"iPhone 17 Pro Max": DeviceDescriptor;
"iPhone 17 Pro Max landscape": DeviceDescriptor;
"iPhone 17e": DeviceDescriptor;
"iPhone 17e landscape": DeviceDescriptor;
"Kindle Fire HDX": DeviceDescriptor;
"Kindle Fire HDX landscape": DeviceDescriptor;
"LG Optimus L70": DeviceDescriptor;
"LG Optimus L70 landscape": DeviceDescriptor;
"Microsoft Lumia 550": DeviceDescriptor;
"Microsoft Lumia 550 landscape": DeviceDescriptor;
"Microsoft Lumia 950": DeviceDescriptor;
"Microsoft Lumia 950 landscape": DeviceDescriptor;
"Nexus 10": DeviceDescriptor;
"Nexus 10 landscape": DeviceDescriptor;
"Nexus 4": DeviceDescriptor;
"Nexus 4 landscape": DeviceDescriptor;
"Nexus 5": DeviceDescriptor;
"Nexus 5 landscape": DeviceDescriptor;
"Nexus 5X": DeviceDescriptor;
"Nexus 5X landscape": DeviceDescriptor;
"Nexus 6": DeviceDescriptor;
"Nexus 6 landscape": DeviceDescriptor;
"Nexus 6P": DeviceDescriptor;
"Nexus 6P landscape": DeviceDescriptor;
"Nexus 7": DeviceDescriptor;
"Nexus 7 landscape": DeviceDescriptor;
"Nokia Lumia 520": DeviceDescriptor;
"Nokia Lumia 520 landscape": DeviceDescriptor;
"Nokia N9": DeviceDescriptor;
"Nokia N9 landscape": DeviceDescriptor;
"Pixel 2": DeviceDescriptor;
"Pixel 2 landscape": DeviceDescriptor;
"Pixel 2 XL": DeviceDescriptor;
"Pixel 2 XL landscape": DeviceDescriptor;
"Pixel 3": DeviceDescriptor;
"Pixel 3 landscape": DeviceDescriptor;
"Pixel 4": DeviceDescriptor;
"Pixel 4 landscape": DeviceDescriptor;
"Pixel 4a (5G)": DeviceDescriptor;
"Pixel 4a (5G) landscape": DeviceDescriptor;
"Pixel 5": DeviceDescriptor;
"Pixel 5 landscape": DeviceDescriptor;
"Pixel 6": DeviceDescriptor;
"Pixel 6 landscape": DeviceDescriptor;
"Pixel 6 Pro": DeviceDescriptor;
"Pixel 6 Pro landscape": DeviceDescriptor;
"Pixel 6a": DeviceDescriptor;
"Pixel 6a landscape": DeviceDescriptor;
"Pixel 7": DeviceDescriptor;
"Pixel 7 landscape": DeviceDescriptor;
"Pixel 7 Pro": DeviceDescriptor;
"Pixel 7 Pro landscape": DeviceDescriptor;
"Pixel 7a": DeviceDescriptor;
"Pixel 7a landscape": DeviceDescriptor;
"Pixel 8": DeviceDescriptor;
"Pixel 8 landscape": DeviceDescriptor;
"Pixel 8 Pro": DeviceDescriptor;
"Pixel 8 Pro landscape": DeviceDescriptor;
"Pixel 8a": DeviceDescriptor;
"Pixel 8a landscape": DeviceDescriptor;
"Pixel 9": DeviceDescriptor;
"Pixel 9 landscape": DeviceDescriptor;
"Pixel 9 Pro": DeviceDescriptor;
"Pixel 9 Pro landscape": DeviceDescriptor;
"Pixel 9 Pro XL": DeviceDescriptor;
"Pixel 9 Pro XL landscape": DeviceDescriptor;
"Pixel 10": DeviceDescriptor;
"Pixel 10 landscape": DeviceDescriptor;
"Pixel 10 Pro": DeviceDescriptor;
"Pixel 10 Pro landscape": DeviceDescriptor;
"Pixel 10 Pro XL": DeviceDescriptor;
"Pixel 10 Pro XL landscape": DeviceDescriptor;
"Moto G4": DeviceDescriptor;
"Moto G4 landscape": DeviceDescriptor;
"Desktop Chrome HiDPI": DeviceDescriptor;
"Desktop Edge HiDPI": DeviceDescriptor;
"Desktop Firefox HiDPI": DeviceDescriptor;
"Desktop Safari": DeviceDescriptor;
"Desktop Chrome": DeviceDescriptor;
"Desktop Edge": DeviceDescriptor;
"Desktop Firefox": DeviceDescriptor;
[key: string]: DeviceDescriptor;
}
3. 自分でPlaywrightを実装してみる
前回投稿した記事で、Spring Securityを使った簡単なログイン機能付きのWebアプリケーションを開発したので、これを使ってUIテスト自動化をやってみたいと思います。
これは実施する環境によりけりですが、今回の私はローカルのUbuntu-24.04(WSL2)にあるPlaywrightから、ローカルのIDEのEclipseで起動しているSpring Bootアプリケーションに対してテストを行おうとしたのですが、私のWSL2の設定がNATモードになっており、その場合アクセスするURLが「http://localhost:8080/」ではアクセスできなかったため、以下のように明示的にWSL2から見たWindowsホストのIPアドレスを特定して、「localhost」の部分をそのIPアドレスに置き換えることで対応できました。
ip route show | grep -i default | awk '{ print $3}'
# 以下のようなIPアドレスが返ってくる
172.30.96.1
まずは動作確認ということで、簡単なテストのみ実施してみます。
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('http://172.30.96.1:8080/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/ログイン/);
});
テストを実行して無事に成功したことを確認しました。
npx playwright test
Running 3 tests using 3 workers
3 passed (5.2s)
To open last HTML report run:
npx playwright show-report
4. テスト実行環境を分ける
さて、このまま実装を進めてもいいのですが、毎回URLを「http://172.30.96.1:8080/」と書くのは面倒くさすぎるのと、これをサーバ上で実施したい場合どうすればいいのか?など、運用的にも問題が出てきます。
ということで環境別にテスト実行が容易にできる構成を今のうちにやっておきます。
いろいろと方法はあるそうですが、今回は「dotenv」を使った方法でやります。
4-1. dotenv-cliのインストール
以下のコマンドでdotenv-cliをインストールする。
npm install --save-dev dotenv-cli
4-2. プロジェクトルートに「.env.local」を作成する
以下のように基本となるURLを記述する
BASE_URL=http://172.30.96.1:8080
4-3. playwright.config.tsを修正する
playwright.config.tsを開き、defineConfig()の中のuseプロパティに「baseURL: process.env.BASE_URL,」を追記する。
export default defineConfig({
// ~省略~
use: {
+ baseURL: process.env.BASE_URL,
// ~省略~
},
4-4. テストコードを修正する
テストコードのURLをべた書きしていた部分を、パスのみに修正する。
(今回の例だと「/」にする)
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
- await page.goto('http://172.30.96.1:8080/');
+ await page.goto('/');
// Expect a title "to contain" a substring.
await expect(page).toHaveTitle(/ログイン/);
});
4-5. package.jsonに環境別テスト実行コマンドを登録する
以下のように、dotenvのコマンドを頭に付けて「-e」オプションで環境名を指定してテストを実行することもできます。
npx dotenv -e .env.local -- npx playwright test
ただし、このコマンドを毎回打つのも長いので、package.jsonのscriptsに以下のように登録しておいて、簡単に呼び出せるようにしておきます。
{
"name": "hello-playwright",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
+ "test:local": "dotenv -e .env.local -- playwright test"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.62.0",
"@types/node": "^26.1.1",
"dotenv-cli": "^11.0.0"
}
}
4-6. テストを実行する
これまでの設定をしたことにより、以下の短いコマンドだけでローカル環境のテストを実行できるようになります。
npm run test:local
この要領で「local」「dev」「stg」など環境別にファイルを分けたらCI/CDのパイプラインに組み込むことも簡単にできそうな気がしますね。
5. テストコードをすっきりさせる
環境変数の外出しもできたので、次はテストコードをすっきり書いていきたいです。
今回作成したコード全体はこちら↓
import { expect, Page, test } from '@playwright/test';
const ADMIN_EMAIL = 'admin@example.com';
const PASSWORD = 'password';
const DISPLAY_NAME = 'aaa';
async function openLoginPage(page: Page): Promise<void> {
await page.goto('/');
}
async function login(
page: Page,
email: string,
password: string
): Promise<void> {
await page.getByLabel('メールアドレス', { exact: true }).fill(email);
await page.getByLabel('パスワード', { exact: true }).fill(password);
await page
.getByRole('button', { name: 'ログイン', exact: true })
.click();
}
async function logout(page: Page): Promise<void> {
await page
.getByRole('button', { name: 'ログアウト', exact: true })
.click();
}
async function openSignupPage(page: Page): Promise<void> {
await openLoginPage(page);
await page.getByRole('link', { name: '新規登録' }).click();
await expect(
page.getByRole('heading', { name: 'ユーザー登録' })
).toBeVisible();
}
async function registerUser(
page: Page,
user: {
email: string;
displayName: string;
password: string;
}
): Promise<void> {
await page
.getByLabel('メールアドレス', { exact: true })
.fill(user.email);
await page
.getByLabel('表示名', { exact: true })
.fill(user.displayName);
await page
.getByLabel('パスワード', { exact: true })
.fill(user.password);
await page
.getByLabel('パスワード確認', { exact: true })
.fill(user.password);
await page.getByRole('button', { name: '登録' }).click();
}
test.describe('ログイン', () => {
test.beforeEach(async ({ page }) => {
await openLoginPage(page);
});
test('ログイン画面のタイトルが表示される', async ({ page }) => {
await expect(page).toHaveTitle(/ログイン/);
});
test('正しい認証情報でトップ画面へ遷移できる', async ({ page }) => {
await login(page, ADMIN_EMAIL, PASSWORD);
await expect(
page.getByRole('heading', { name: 'トップ画面' })
).toBeVisible();
});
test('誤ったパスワードではログインできない', async ({ page }) => {
await login(page, ADMIN_EMAIL, 'passwor');
await expect(page).toHaveURL(/\/login\?error$/);
await expect(
page.getByText(
'メールアドレスまたはパスワードが正しくありません。',
{ exact: true }
)
).toBeVisible();
});
});
test.describe('ログアウト', () => {
test('ログイン後にログアウトできる', async ({ page }) => {
await openLoginPage(page);
await login(page, ADMIN_EMAIL, PASSWORD);
await expect(
page.getByRole('heading', { name: 'トップ画面' })
).toBeVisible();
await logout(page);
await expect(
page.getByText('ログアウトしました。', { exact: true })
).toBeVisible();
await expect(page).toHaveURL(/\/login\?logout$/);
});
});
test.describe('ユーザー登録', () => {
test('新規ユーザーを登録してログインできる', async ({ page }) => {
const user = {
email: `aaa${Date.now()}@example.com`,
displayName: DISPLAY_NAME,
password: PASSWORD,
};
await openSignupPage(page);
await registerUser(page, user);
await expect(page).toHaveURL(/\/login$/);
await expect(
page.getByText(
'ユーザー登録が完了しました。登録したアカウントでログインしてください。',
{ exact: true }
)
).toBeVisible();
await login(page, user.email, user.password);
await expect(
page.getByText(`ようこそ、${user.displayName}さん`, {
exact: true,
})
).toBeVisible();
await logout(page);
await expect(page).toHaveURL(/\/login\?logout$/);
});
test('8文字未満のパスワードでは登録できない', async ({ page }) => {
const user = {
email: `aaa${Date.now()}@example.com`,
displayName: DISPLAY_NAME,
password: 'passwor',
};
await openSignupPage(page);
await registerUser(page, user);
await expect(page).toHaveURL(/\/signup$/);
});
});
5-1. 同じ処理は1つにまとめる
これはPlaywrightに限らず、プログラミング全般にいえる当たり前のことですが、テストコードはそこまで頭が回らないことも多く、ついべたっと書きがちになってしまいます。
// Bad(悪い例)
test('ログインをしてから〇〇をする', async ({ page }) => {
await page.goto('/');
await page.getByLabel('メールアドレス', { exact: true }).fill('admin@example.com');
await page.getByLabel('パスワード', { exact: true }).fill('password');
await page.getByRole('button', { name: 'ログイン', exact: true }).click();
// ~省略~
});
test('ログインをしてから〇〇が表示されること', async ({ page }) => {
await page.goto('/');
await page.getByLabel('メールアドレス', { exact: true }).fill('admin@example.com');
await page.getByLabel('パスワード', { exact: true }).fill('password');
await page.getByRole('button', { name: 'ログイン', exact: true }).click();
// ~省略~
});
これをこんな感じにすると可読性も保守性も上がるのではないでしょうか?
// Good(良い例)
// 複数使う定数値はトップで宣言しておく
const ADMIN_EMAIL = 'admin@example.com';
const PASSWORD = 'password';
// トップ画面にアクセスをするだけの関数
async function openLoginPage(page: Page): Promise<void> {
await page.goto('/');
}
// ログイン入力をするだけの関数
async function login(page: Page, email: string, password: string): Promise<void> {
await page.getByLabel('メールアドレス', { exact: true }).fill(email);
await page.getByLabel('パスワード', { exact: true }).fill(password);
await page.getByRole('button', { name: 'ログイン', exact: true }).click();
}
// テスト
test.describe('ログイン', () => {
test.beforeEach(async ({ page }) => {
await openLoginPage(page);
});
test('ログインをしてから〇〇をする', async ({ page }) => {
await expect(page).toHaveTitle(/ログイン/);
});
test('ログインをしてから〇〇が表示されること', async ({ page }) => {
await login(page, ADMIN_EMAIL, PASSWORD);
// ~省略~
5-2. Playwrightテストライブラリ
前項でもさらっと出ましたが、テストを一つの単位にまとめるtest.describe()や、毎テストの前に必ず実行してくれるtest.beforeEach()などなど、ほかにもさまざまな便利な関数が用意されています。
これらを全部紹介するとキリがないので、詳細は以下の公式ガイドを参照してください。
5-3. POM(Page object models)
今回は簡単な内容だったので、1つのテストファイルに以下のようにざざっとテスト用関数を書いていきました。
await login(page, email, password);
await logout(page);
await registerUser(page, user);
簡単なアプリであればこれでも十分かもしれません。
しかし、例えばログイン画面のHTMLのinput要素のid属性名が変わったりすると、上記のように個別の関数として書き出していると全部を修正する必要に駆られるかもしれません。
ましてや実務ともなるとそもそも画面数がどえらい数あると思います。
そこで、画面(Page)そのものをオブジェクトとして表現できないか?という考え方で「POM(Page object models)」というものがあります。
例えば、上記の例だと「LoginPage」というクラスを作ってしまって、そこに「メールアドレス入力欄」「パスワード入力欄」「ログインボタン」など、その画面を構成する要素をすべて持ちます。
class LoginPage {
async open() {
...
}
async login(email, password) {
...
}
}
const loginPage = new LoginPage(page);
await loginPage.open();
await loginPage.login(
"admin@example.com",
"password"
);
こうすることで、プロダクションコードに変更が入っても、既存のテストコードには影響を与えず、POMクラスだけ修正をする、みたいなこともできそうなものです。
これも全部を紹介するとキリがないので、詳細は公式ガイドを参照してください。
5-4. Fixtures
POMと同じぐらい非常に重要な概念の「Fixtures」です。
公式ガイドには以下のように記載されています。
Playwright Testは、テストフィクスチャという概念に基づいています。テストフィクスチャは、各テストの環境を確立するために使用され、テストに必要なものだけを提供し、それ以外のものは一切提供しません。テストフィクスチャは、テスト間で独立しています。フィクスチャを使用することで、共通の設定ではなく、テストの意味に基づいてテストをグループ化できます。
要するに「テスト開始時に必要なものを自動で用意する」というものが「Fixtures」です。
今回のテストコードでは毎回以下のように書いていました。
test(..., async ({ page }) => {
これは毎回Playwrightがpageを用意してくれてるのですが、まさにこれ自体が「Fixture」とのことです。
このFixtureは自分で定義することも可能で、例えば「loggedInPage」というFixtureを作ってしまえば、「page作成 → ログイン → トップ画面」という処理自体を1つのFixtureにすれば、
test(..., async ({ loggedInPage }) => {
とできて、さらに柔軟な対応が可能になるとのことです。
- POMは、画面自体を表していて、
- Fixtureは、状態を表している
とも言えそうですね。
こちらも全部を紹介しようとすると、それ一つで記事が書けそうなぐらい深い内容になりそうなので、引き続き詳細は公式ガイドを参照してください。
備考
備考1. テスト対象ファイル名は「*.test.ts」または「*.spec.ts」である
npm init playwright@latestで自動で作成されるテンプレートのテスト対象ファイル名は「example.spec.ts」となっていました。
このテスト対象となるファイル名はデフォルト設定では「*.test.ts」または「*.spec.ts」にしなければいけません。
もし、自前でテスト対象ファイルをカスタマイズしたい場合は、playwright.config.tsのdefineConfig()でtestMatchプロパティで指定することが可能です。
備考2. テスト対象ディレクトリは設定で変えることができる
自動で作成されるテンプレートのテスト対象ファイルはtestsというディレクトリ配下にありました。
これも前項と同じく、playwright.config.tsのdefineConfig()でtestDirプロパティで指定することが可能です。
おわりに
今回はPlaywrightを初めて触ってみた第一歩だったので、ざっくりとしかできていませんが、かなり便利だなと思いました。
後半に紹介したPOMやFixtureも使ってみたいですが、ここまでいくと、もはや開発チームとは別に独立したテスト専門のQAチームがいないとなかなか大変な気もしますね。
とはいえ、画面のテストを自動化できて、さらに複数のブラウザにも対応できて、その上これをCI/CDのパイプラインに組み込むことも可能なので、こんな素晴らしいことはどんどんやっていきたいな~と思いました。
今後はこれをGithub ActionsなどのCI/CDのパイプラインに組み込んで使うやり方も勉強していきたいと思います。
