Playwrightを試してみた
皆様、E2Eテストを導入していますか?
今Microsoft製E2Eツール「Playwright」がアツいので、検証してみました。
公式サイト:https://playwright.dev/
環境とソフトウェアのバージョン
- OS: macOS Monterey 12.4
- Next.js: 12.1.6
- Node: 16.13.1
- React: 18.1.0
- Playwright: 1.24.1
- TypeScript: 4.6.2
Playwrightのインストール
自動インストール(initコマンドを使用)、手動インストールの2通りの方法があります。
自動インストール(initコマンドの使用)
# npm
npm init playwright@latest
# yarn
yarn create playwright
構成ファイルが作成されます。テストディレクトリを指定し、GitHub Actions workflowを追加するかを選択します。テストサンプルとして、指定したテストディレクトリ配下に example.spec.ts が追加されます。
ブラウザはChromium、Firefox、Webkitが自動でインストールされます。
手動インストール
選択したブラウザのみをインストールしたい場合は手動インストールがおすすめです。
# npm
npm install -D @playwright/test
# yarn
yarn add -D @playwright/test
次に、Playwrightでサポートされているブラウザをインストールします。
デフォルトでは、Chromium、Firefox、Webkitがインストールされます。
# npx
npx playwright install
# yarn
yarn playwright install
例えば、chromiumのみをインストールしたい場合は以下のコマンドを実行することでchromiumのみをインストールすることができます。
npx playwright install chromium
playwright.config.ts ファイルの作成
configファイルを作成します。
playwright.config.ts
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices } from "@playwright/test";
const config: PlaywrightTestConfig = {
testDir: "./tests",
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
};
export default config;
※様々な設定項目があるため、詳しくは公式ドキュメントを参照
package.jsonの設定
テスト実行用のscriptを追加します。
package.json
{
"name": "playwright",
"version": "0.1.0",
.
.
.
"scripts": {
"dev": "next dev",
.
.
.
"test:e2e": "playwright test", // 追加
// playwright.config.tsを別のディレクトリに作成した場合は以下でファイルの指定が可能
"test:e2e": "playwright test -c ../playwright/playwright.config.ts"
}
}
テスト検証用のページを作成
今回はTopページからボタンをクリックするとAboutページへ遷移することをテストします。
ソースコード
/src/pages/index.tsx
import { useRouter } from "next/router";
export default function Login() {
const router = useRouter();
return (
<div>
<p>Topページ</p>
<button
onClick={async () => {
await router.push("/about");
}}
data-testid="about-button"
>
Aboutページへ移動
</button>
</div>
);
}
export default function About() {
return (
<div>
<p>Aboutページ</p>
</div>
);
}
画面イメージ
data-testidについての補足
上記で作成したindex.tsxファイルでdata-testid
という属性を定義しました。
テストの実行は、id
やclass
を指定して要素を取得することも可能ですが、特殊なclassを設定する等、テストのためだけにソースコードを改修すると、例えばページ遷移のテストがログインボタンの色を変える度に落ちるという問題が発生することが考えられます。
このようにテスト対象のロジックとは無関係なコードにテストは依存すべきではありません。
そこで、テストのためだけのカスタムデータ属性のdata-testid
を使用することによってソースコードとテストを分離します。
その結果、テストを意識することはなくソースコードの改修が可能になりますし、テストも要素の取得元がdata-testid
属性と明確なため、書きやすくなります。
テストファイルの作成と実装
Topページのテストファイルを作成します。
/tests/e2e/index.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Top画面", () => {
test.beforeEach(async ({ page }) => {
await page.goto("./");
});
test("Aboutボタンが存在すること", async ({ page }) => {
const aboutButton = page.locator("data-testid=about-button"); // ※
expect(aboutButton).toHaveCount(1);
await expect(aboutButton).toHaveText("Aboutページへ移動");
});
test("Aboutボタンクリックで、Aboutページに遷移すること", async ({ page }) => {
const aboutButton = page.locator("data-testid=about-button");
await aboutButton.click();
await expect(page).toHaveURL("./about");
});
});
※data-testid
を指定してAboutボタンの要素を取得
テストの実行
テストを実行します。
# npm
npm run test:e2e
# yarn
yarn run test:e2e
ページ遷移以外のテストの紹介
example1
ログインページでバリデーションに失敗した時にエラーメッセージが表示されることをテストする
ソースコード(※Props等省略)
<div>
<input
id="email"
type="email"
value={value}
onChange={onChange}
data-testid="email"
/>
{hasError && (
<ul data-testid="email-errors">
{errors?.map((message, i) => (
<li key={i}>
{message}
</li>
))}
</ul>
)}
</div>
<div>
<input
id="password"
type="password"
value={value}
onChange={onChange}
data-testid="password"
/>
{hasError && (
<ul data-testid="password-errors">
{errors?.map((message, i) => (
<li key={i}>
{message}
</li>
))}
</ul>
)}
</div>
<div>
<button
onClick={onClickLogin}
data-testid="login-button"
>
ログイン
</button>
</div>
テストコード
test("バリデーション不合格でログインボタンをクリックした場合、エラーメッセージが表示されること", async ({
page,
}) => {
const email = page.locator("data-testid=email"); // フォームの要素を取得
const password = page.locator("data-testid=password");
await email.fill(""); // フォームを空にする
await password.fill("");
const loginButton = page.locator("data-testid=login-button");
await loginButton.click();
await expect(loginButton).toBeDisabled();
await expect(page.locator("[data-testid=email-errors] li")).toHaveText(
"必須入力項目です",
);
await expect(page.locator("[data-testid=password-errors] li")).toHaveText(
"必須入力項目です",
);
});
example2
CSSクラスを持っていることをテストする
ソースコード
<div class="primary wFull" data-testid="hello-world">Hello World</div>
テストコード
test("Aboutページとテキストが表示されていること", async ({ page }) => {
const helloWorld = page.locator("data-testid=hello-world");
await expect(helloWorld).toHaveClass(/primary/); // 緩和された正規表現で部分一致
await expect(helloWorld).toHaveClass("primary wFull"); // 完全一致
});
※アサーションは様々なものがあります。詳しくは公式ドキュメントを参照
感想
フロント側のテストといえば今まで手動で行うことも多かったのですが、自動化できると便利ですね。
前回、弊社のtakagi先生がPlaywrightでのコンポーネントテストの記事を書いていて、とても勉強になったので、そちらもぜひ読んでみてください。
PlaywrightでのNext.jsのコンポーネントテストでパスエイリアスを解決する|Next.js|開発ブログ|株式会社Nextat(ネクスタット)
ありがとうございました。