ウェブアプリケーションにおけるエンドツーエンド(E2E)テストについて、基礎的な考え方をまとめたものです。フレームワークはいくつもありますが、比較した中では WebdriverIO が始めやすいと考えて、その使い方もまとめました。
テストの目的
エンドツーエンドテストはシステム全体を対象とするテストです。内部はブラックボックスとみなして、ある操作による結果はどうなるのかという、システムの振る舞いの正しさを検証します。
エンドツーエンド
システムにおける「エンドツーエンド」とは、一般的に入力(操作)と出力(結果)を意味します。ウェブアプリケーションでは基本的に、入力はウェブブラウザへの操作で出力はウェブブラウザの表示となります。つまりウェブブラウザによるインターフェーステストとなります。
ウェブブラウザから操作をテストすることから、内部はブラックボックスとすることが基本です。ブラウザに現れないような細かい部分のテストは、結合テストや単体テストなど、それ相応の細かいテストですでに検証されていることを前提とします。
エンドツーエンドテストは実行に時間がかかりがちです。テストケースが増えると維持が負担となってしまいますので、重要な機能に絞ってテストを作ることになります。
WebdriverIO
ウェブブラウザによるテストを自動的に実行する WebdriverIO ライブラリがあります。もともとウェブブラウザへの操作を自動的にプログラムできるSelenium WebDriver(単純に WebDriver とも)という Java のライブラリがありますが、これを node.js から利用しやすくしたライブラリが WebdriverIO です。
WebDriver
ウェブブラウザの操作が自動的に行われるようにするためには、そのブラウザを擬似的に操作するプログラムが必要です。このプログラムが「ドライバー」と呼ばれるものです。
そしてウェブブラウザには Firefox や Chrome といった複数の種類があり、擬似的に操作する方法もそれぞれ異なります。そのためドライバーも、FirefoxDriver であったり ChromeDriver であったりブラウザごと(さらにはバイナリ実行環境ごと)に存在します。
ただしドライバーの種類は違っても「リンクをクリックする」「フィールドに入力する」といった目的の操作は一緒です。このような共通の操作をまとめて、独自となる処理部分を隠したインターフェースが WebDriver です。
Selenium Server
WebDriver に指示を与えるためには Java プログラムを実行することになります。Java の実行に必要な JVM は起動が遅いため、Java プログラム外からの都度の呼び出しは実行効率が良くありません。そのため、指示を受け付けるサーバープログラムが用意されています。このサーバープログラムが Selenium Server です。
Selenium Server は JSON による HTTP リクエストで指示を受け付けています。これは WebDriver Wire Protocol として標準化がなされています。
WebdriverIO はこの Selenium Server とのやりとり部分を隠れて行います。さらにテストとしての可読性と記述性を高めた API を用意してくれているので、もとの Selenium WebDriver よりもプログラマーには扱いやすくなっています。
ところで Selenium Server アプリケーションそのものは WebdriverIO では基本的に取り扱い対象外となっています。そこで、npm ライブラリーの selenium-standalone を利用して、サーバーアプリケーションをインストールします。テストしたいウェブブラウザと同じ環境にインストールします。
テスト実行前には Selenium Server を起動しておく必要があります。selenium-standalone でも起動を扱えますが、wdio-selenium-standalone-service を利用することで、WebdriverIO 実行時に自動的に Selenium Server の起動や停止を行ってくれるようになります。
WebdriverIO と Selenium Server の実行環境が異なるとき、例えば VM 上やリモートサーバーにアプリケーションがあるときなどは、お互いが通信できるようサーバーアドレスやポート番号に気を配る必要があります。同一環境で実行していればそのまま使えます。
書き方
エンドツーエンドテストは複数のプログラムコンポーネントが関わるため書き方によっては変更に弱く壊れやすいテストになってしまいます。ほかにもテストはシステム全体を検証するので、技術的にはもちろんですが、ビジネス領域や利用者行動などといった技術外の都合といった、書くときに気をつけたい要因が多くあります。
そのためよいテストケースを作るには、システム要件やユーザーストーリーといった上層の概念を持ち込むと良いと考えています。プロダクトオーナーの期待に応えているか、利用者にわかりやすいシステムになっているかなどをテストに盛り込めるためです。
振る舞い駆動
システムのどの関係者にとっても意味のあるテストにするため、結果を評価する側にも優しいことがテストとして望ましくなります。それが目指せる開発技法に振る舞い駆動開発(Behavior Driven Development、BDD)があります。
これはシステムの振る舞いに重点をおくもので、内部がブラックボックスであっても構いません。その検証コードの書き方には特徴があり、検証対象の特徴を自然言語で述べるような書き方をします。非技術者にも読みやすいテストレポートが作れますので、エンドツーエンドテストとの相性がとても良くなっています。
振る舞い駆動開発では検証コードを実装よりも先に用意するのですが、エンドツーエンドテストは各コンポーネントができあがってから作ることが実情なので、厳格に従うことは難しい面もあります。そのため「BDD 風に書く」ことになるでしょう。
WebdriverIO では Mocha による BDD スタイルの記述法が標準設定となっています。そしてspec レポーターを使えば、自然言語風のテスト結果が得られるようになります。書き方の基準はこの結果レポートが非技術者にも読みやすいかとするとよいでしょう。
テストスイート
とある機能を検証するにあたって、ひとつ画面が開始点であったり主体であることがほとんどだと思います。ですので、テストスイート(テストファイル)は画面単位で分けることを基本とすると扱いやすいと思います。おおもとの describe()
に画面名を記述するとよいでしょう。
describe('webdriver.io API ページ', () => {
// 個々にテストケースを記述...
})
ここでいう画面とは必ずしも URL パス(ページ)と一致しないこともあります。フォーム送信などでページが遷移すると表示は近いながらも URL パスが異なることもあります。一方で同じ URL でもログイン前ログイン後といったように表示が同一ではないこともあります。この場合もテスト結果レポートが読みやすくなるかを基準に考えるのも一案です。
テストケース
BDD スタイルでは個々のテストケースは it()
メソッドで記述することになります。実際に書き進めるにあたって、検証したい振る舞いごとに個々のテストケースを分けるとよいでしょう。it()
にはテストケース名を与えます。「『should』で始める」と言われていますが、直訳した日本語で「〜べき」とすると堅い印象になることもあります。「〜する」という終わり方が日本語としては自然だと個人的には考えています。
describe('webdriver.io API ページ', () => {
it('コマンド名を検索欄に入力すると絞り込まれる', () => {
browser.url('http://webdriver.io/api.html')
$('input[placeholder="Search..."]').setValue('getC')
assert(!$('=setCookie').isVisible(), 'setCookie が非表示となる')
assert(!$('=deleteCookie').isVisible(), 'deleteCookie が非表示となる')
assert($('=getCookie').isVisible(), 'getCookie は表示されたまま')
})
})
とある条件において検証したいことが複数できたときは、グループ化するとよいでしょう。その場合は describe()
や context()
を使います。テストコードと共にテスト結果も見やすくなるでしょう。また共通で使う UI コンポーネントや前処理は beforeEach
にあらかじめ取り出しておくこともできます。
describe('webdriver.io API ページ', () => {
describe('コマンド一覧の表示', () => {
beforeEach(() => {
browser.url('http://webdriver.io/api.html')
this.serachField = $('input[placeholder="Search..."]')
this.deleteCookieLink = $('=deleteCookie')
this.setCookieLink = $('=setCookie')
this.getCookieLink = $('=getCookie')
})
it('Cookie 関連コマンドを表示する', function () {
assert(this.deleteCookieLink.isVisible(), 'deleteCookie が表示されている')
assert(this.setCookieLink.isVisible(), 'setCookie が表示されている')
assert(this.getCookieLink.isVisible(), 'getCookie が表示されている')
})
it('コマンド名を検索欄に入力すると絞り込まれる', function () {
this.serachField.setValue('getC')
assert(!this.setCookieLink.isVisible(), 'setCookie が非表示となる')
assert(!this.deleteCookieLink.isVisible(), 'deleteCookie が非表示となる')
assert(this.getCookieLink.isVisible(), 'getCookie は表示されたまま')
})
})
})
DOM 要素の取得方法
ブラウザ操作を記述するにあたって、ボタンやフィールドなどといった DOM(HTML)要素を取得することになります。この際には、利用者の目に見えている要素を利用することが重要だと考えています。ブラウザを操作するときに利用者は見えているもので判断しているはずだからです。テストを書くプログラマーにとっても後日修正するときにわかりやすくなります。
要素を取得するときは CSS セレクターが最も利用しやすいと思います。指定の際はセレクターが浅くなるように心がけると良いでしょう。コードとして読みやすくなりますし、ビューコード(HTML 構造)の変更に強くなります。そして利用者に画面操作を説明するように読めるテストコードだと、維持がしやすくなると個人的には考えています。
CSS 属性値セレクターを活用する
CSS セレクターの中でも属性値セレクターは、維持や修正がしやすいと経験上感じています。たとえば画像は img[src="path/to/title.png"]
というように指定できますので、「タイトル画像が表示されている」という意味がコード上でわかりやすくなります。
疑似クラスも利用価値が高いです。「ページ先頭の画像をクリック」、「上から 3 番目の項目に『○○』が表示されている」といった意味がコードで表せるためです。
一方で ID セレクターやクラスセレクターは利用を避ける方が維持しやすいと考えています。これらは画面表示から識別できるものではないためです。ほかにもテストに利用したいがためにやみくもに追加してしまいがちなのと、それを記述するビューコードがテストコードから離れていることから維持管理の負荷が高いことも理由です。
XPath のテキスト選択を活用する
XPath はリンクテキストやテキストの部分一致で要素を選択できます。たとえば「『送信』と書かれたボタン」や「『名前』というラベルの横にある入力ボックスに『鈴木一郎』と入力」というような、ブラウザ表示に沿った要素の取得がコードで表現できるようになります。
XPath はとっつきにくいですが、このテキストマッチだけは利用価値があると考えています。
REPL で browser
オブジェクトをさわる
検証コードを書き始めるときは REPL 機能を使うと便利です。ターミナルに表示されるコマンドプロンプトから JavaScript コードを実行することができます。browser
オブジェクトから DOM 要素が取得できているか、検証する値が期待通り得られるかを実際に確かめることができます。
エラー対処
テストを書いた当初はうまくいっていても、その後のアプリケーションコード修正によってテストがエラーになってしまうことがよくあります。修正にはなぜテストが失敗するかを調べなければなりませんが、実行が遅いことから苦痛が伴うこともしばしばあります。その対処法を説明します。
スクリーンショットを活用する
WebdriverIO ではテスト失敗時にスクリーンショットを自動的に撮影しておいてくれる機能があります。クライアントのスクリプト実行のテストが失敗しているときに役立ちます。
任意でスクリーンショットを撮っておくこともできますので、ヘッドレスモードで実行しているときのデバッグにも有効です。
browser.debug()
browser.debug()
をコード中に入れるとそこでテスト実行を止めることができます。ブラウザのインスペクターを使ってデバッグができるようになります。さらに REPL 状態になるため、JavaScript コードを入力することもできます。
一時的に止まっていても、テストランナーによるタイムアウトが発生すると、テスト全体が終わってしまうことに注意してください。--mochaOpts.timeout 600000
(10 分でタイムアウト)など、タイムアウト時間を長くするオプションを加えてから実行すると良いでしょう。
多めにアサーションを入れておく
原因がすこしでも特定しやすくなるようにあらかじめ備えておくことも大切です。
たとえば使用する DOM 要素は取得したらそれは変数に格納しておき、isVisible()
で表示されて(存在して)いるかや、getText()
で表示文字列が期待通りかを確かめておくことなどがあげられます。
--spec
と --watch
オプション
--spec
は指定したファイルのテストだけを実行することができます。コードを直したいテストだけを指定することで、すばやく検証を終わらせられます。
--watch
はファイル変更を検知して変更されたテストだけを実行します。ファイル修正後に素早くテストが実行されるので、テストコードのトライ&エラーを繰り返したいときに便利です。
.only
と .skip
メソッド
Mocha では特定のテストメソッドだけ、.only
をつけることで実行ができます。逆に指定のメソッドだけを飛ばすには .skip
をつけます。
データベースと外部システム
テストしたいウェブアプリケーションは副作用を持つことがほとんだと思います。データベースやその他外部システムに対して変更を与えるときは、テスト開始前にそれらの初期化処理が必要です。テストを実行するとその内部状態が変わり、テストを繰り返すとそれが失敗する原因になるためです。この初期化処理はテストスイートごとに行うと良いでしょう。
データベースについてはエンドツーエンドテスト用のものを用意しておく必要があります。そしてエンドツーエンドテストでアクセスするためのホスト名を用意して、そこへのアクセスではテスト専用のデータベースなどに接続されるようにウェブアプリケーション側の整備も行っておきます。
初期化はデータベースなどに接続して行うことになります。接続先がネットワーク上にあるときは SSH 接続であったり認証情報の管理をテスト専用にも備えておきます。SQL を流し込むカスタムコマンドを作っておくと良いでしょう。
モバイル端末向けサイトのテスト
モバイル端末向けサイトをテストするには、基本的にシミュレーターに対して Appium を利用することになります。ユーザーエージェントを識別してモバイル端末向けに専用のデザインを表示しているサイトであれば、WebdriverIO への設定でユーザーエージェントを改変することで、Appium を利用すること無くテストを行えます。
さいごに
さいごに、今回のフレームワークの選択方法を語ります。ブラウザ自動化のフレームワークはたくさんあるので選択に迷うところです。
私の場合は実ブラウザでテストをしたかったので Selenium を利用するフレームワークにすることにしました。プラットフォームは本当は JVM にしたかったところですが毛嫌いされがちなので、どのウェブ開発でも使うことになるであろう JavaScript で書ける node.js を選びました。
個々のフレームワークを比較すると、Nightwatch.js はメソッドチェーンにアサーションを入れるという書き方を許すのはメンテのしにくさを呼びそうだなという印象です。Protractor はロケーターが element
や by
といった Selenium のオリジナルを踏襲しているところが良いところですが、同時に扱いづらさに不安を持ってしまいます。WebdriverIO はそれらのあたりを適度に抽象化しているところが気に入っています。TestCafe は Selenium (JVM) を避けられるので、async await なしで書けるようになってくれれば代替手段候補です。
ただ基本的な考え方は同じはずですので、スタイルに合っていれば好みのライブラリーを使ってしまって良いでしょう。