A Test-First Approach with Playwright for Unstable Systems
この記事では、Playwright等のTesting Frameworkを使ったE2EテストでTest-Firstを実現するための手法を紹介します。この手法は、テストの自動化を試みる際の以下の課題を解決します。
- テストコードを実装しながら動作確認をしたいが、テスト対象のシステムがまだ不安定なためなかなか動かない。
- テストコードのテスト内容を非エンジニアに説明できない。
またこの手法は、前回の記事で紹介したE2Eテストのデザインパターン「拡張Page Object Model」の以下の設計を前提としています。
- PageElementの共通基底クラスを作成し、Testing FrameworkのAPIをPageElementから分離し共通基底クラスに集約する。
この記事の動作可能な実装例はGitHubで公開しています。
概要
この手法では、テストコードに以下の機能を持たせて実装することで、前述の課題の解決を試みます。
- ブラウザを操作せずにテストコードを実行する。
- ブラウザに対する操作内容をファイル出力する。
以降ではこの機能をDry Run
と呼びます。Dry Run機能の実行構成を以下に示します。
通常の実行時(real-run)はBasePageElementがBrowserController経由でBrowserを操作し、テストを行います。これに対しDry Run実行時(dry-run)は、BasePageElementはBrowserControllerを使用せず、DryRunLogの出力のみを行います。
これを処理シーケンスで表したものを以下に示します。
- PageElementがBasePageElementに画面要素の操作を指示
- 実行モードの場合
- BasePageElementがBrowserControllerにブラウザの操作を指示
- DryRunモードの場合
- BasePageElementがDryRunにログ出力を指示
- DryRunがDryRunLogファイルに画面要素の操作内容を出力
以降では、コンポーネント間の相互作用とその実装について説明します。
PageElement -> BasePageElement
PageElementはBasePageElementが提供するメソッドを使用して画面要素の操作を行います。
ここでは例としてLoginPagePageElementのclickLoginButtonを挙げます。
import BasePageElement from '@arch/BasePageElement';
export default class LoginPagePageElement extends BasePageElement {
get pageNameKey() {
return 'login';
}
async clickLoginButton() {
await this.click('#login-form input[name="login"]');
}
}
PageElementとBasePageElementとの間ではDryRunは関わってきませんが、PageElementはPlaywrightのAPIを直接は使用せず、必ずBasePageElement経由で使用する、という点が重要です。
BasePageElement -> BrowserController
BasePageElementはBrowserControllerを使用して画面要素の操作を行います。BrowserControllerは実装上ではPlaywright APIの Page です。BasePageElementは、DryRun実行時にはBrowserControllerは使用せずログ出力を行う、という制御を実装します。この制御を入れたBasePageElementのclickメソッドの実装は以下の様なものになります。(ここでは説明のために簡略化した実装にしています。実際の実装はBasePageElement.tsを参照してください。)
import { expect, type Page } from '@playwright/test';
import { Action, DryRun } from '@arch/DryRun';
export default abstract class BasePageElement {
private page : Page;
abstract get pageNameKey(): string;
protected async click(selector: string) {
if (!DryRun.isOn) {
await this.page.locator(selector).click();
} else {
const log = DryRun.log(this.pageNameKey, selector, Action.CLICK);
await this.page.evaluate(`console.log(\`${log}\`);`);
}
}
}
DryRun.logに画面操作の実行箇所・内容を識別するための情報を渡し、ログをファイルに出力します。またそのログを操作中のブラウザのconsole.logにも出力します。これによりログはPlaywrightのTrace Viewerからも参照できるようになります。
click以外のinputTextやselectメソッドにも同様に、DryRunでの分岐とログ出力を実装します。
DryRun
DryRunは通常実行/DryRunの状態管理とログ文字列の構築、ログのファイル出力を行います。実装は以下の様なものになります。(ここでは説明のために簡略化した実装にしています。実際の実装はDryRun.tsを参照してください。)
import fs from 'fs';
export enum Action {
GOTO,
CLICK,
INPUT,
}
export class DryRun {
constructor(public isOn: boolean, private readonly formatter: LogFormatter) {}
log(pageNameKey: string, itemNameKey: string, action: Action, input?: string) {
const pageName = this.resolvePageName(pageNameKey);
const itemName = this.resolveItemName(itemNameKey);
const logStr = this.formatter.format(pageName, itemName, action, input);
fs.appendFileSync(this.filePath, str + '\n');
}
}
DryRunログは「テストシナリオで行われる画面操を把握する」ためのログです。このためログ文字列には以下の情報を含める必要があります。
上記のlogメソッドの引数はそれぞれ以下の意味を持ちます。
- どの画面で(pageNameKey)
- どの画面要素に対し(itemNameKey)
- 何の操作を行ったか(action)
- 何の値を操作に使用したか(input)
以下は、実際のDryRunログです。
トップ画面で「/」に遷移する。
ログイン画面で「ユーザー名」に「admin」を入力する。
ログイン画面で「パスワード」に「newadminpass」を入力する。
ログイン画面で「ログインボタン」をクリックする。
トップ画面で「プロジェクトメニュー」をクリックする。
トップ画面で「デモプロジェクト」をクリックする。
プロジェクト画面で「Work Packagesメニュー」をクリックする。
プロジェクト画面で「作成ボタン」をクリックする。
プロジェクト画面で「タスクリンク」をクリックする。
タスク入力画面で「題名」に「Test Subject onak9fp7aj8」を入力する。
タスク入力画面で「優先度」に「High」を入力する。
タスク入力画面で「保存ボタン」をクリックする。
Dry Run Log on Trace Viewer
BasePageElementでconsole.logに出力したDryRunログは、通常モードで実行した際はPlaywrightのTrace Viewer上で以下の様に出力されます。
実際にDry Runを実行した結果のPlaywirght Report、Trace Viewerは以下で参照できます。
Playwright Report + Trace Viewer
おわりに
Dry Run機能をテストコードに組み込むことで、ブラウザ操作以外のテストコードの処理を実行することが可能になります。つまり、テストコードをテスト対象のシステムにアクセスさせずに実行することが可能になります。これにより、システムが未だ不安定な時期からテストコードを実装することができます。またDry Runログは、テストコードを読まずにテスト内容を把握することができるため、非エンジニアのプロジェクトメンバーにとって有益な資料となります。Dry Run機能の実装例と動作を確認するには以下のGitHubリポジトリを参照してください。
Discussion