Seleniumを使ったWeb UI自動テストシステムの構築でやったことまとめ
Seleniumを使ったWeb UIの自動テストシステムを作ったので、やったこと・感想などをまとめてみます。
テスト対象
テスト対象は、AJAXなWebアプリです。
- サーバーはREST APIを提供するのみで、UIは全てJavaScriptという構成。
- サポートブラウザはIE7以降,Firefox3.6以降。(特定の組織内で使うものなので、Operaなどは未サポートです。)
テストの目的
以下を目的としました。
気をつけたこと
- 拡張性/保守性
- 仕様や内部構造の変更時に、テストの修正が容易であること。
- UI開発では、デザイン変更や文言修正等表示上の仕様変更が多いため、それを前提として保守しやすい設計にしておく必要があります。
- 安定した成功
- テストが常に安定して成功する状態に維持されること。
- 狼少年なテストは使えません。安定して成功することでテストへの信頼が生まれ、リリース時のリグレッションテスト等に活用できるようになります。
- 網羅率(カバレッジ)
- 自動テストの対象範囲をなるべく広くとること。
- 網羅率を高めることで手作業での作業項目が減り、自動化で削減される工数が大きくなります。
- 継続的な実施
- 定期的・継続的にテストを実施することで、実装変更に伴う動作不正を速やかに検出できます。
やったこと
自動テストと単体テストの併用
Seleniumによる自動テストだけではなく、単体テストを併用してテストのカバレッジを上げるようにしました。
単体テストで確認可能な項目は、原理的には、Seleniumによる自動テストでも確認できます。ですが、自動テストだけだと以下のような問題もあります。
- 組み合わせ爆発
- 一般に、結合するモジュールが増えれば増えるほど、テストのバリエーションは掛け算で増えていきます。
- 自動テストは、サーバー-クライアントの結合試験となるため、すべての項目を自動テストで試験しようとすると、漏れが生じてしまったり保守性の観点で問題があるテストになってしまう可能性が高いです。
- 実行可能な操作の制限
- 異常系の動作やクライアント内部で使用するキャッシュの動作確認など、外部からの観測や現象の再現が難しい項目を試験することが難しい/できません。
- 所要時間
ということで、Seleniumによる自動テストに加えて、単体テストも併用する方針としました。
- 入力された数値のフォーマットチェックであれば・・・
- 「入力値が不正かどうか」のチェックは、単体テストですべてのパターンを確認。
- 「不正と判断された場合に、ダイアログでエラーメッセージが表示される」ことは、自動テストで保証。
- 自動テストでは、入力値のバリエーションパターンはチェックしない。
といった感じで、自動テストの負荷を削減できます。もちろん、
- 自動テストをしっかり作れば、単体テストはいらなくね?
- 自動テストするから、クライアント側のコードではテスタビリティとか考えなくていいよね!
というのはNG。
ページ オブジェクト パターンの採用
ページオブジェクトパターンを採用して、機能追加や内部実装変更時のテストの追従コストを最小化しました。
ページオブジェクトは、テスト対象となる「画面」や「ダイアログ」といったものをクラスとして抽象化したものです。
- 「画面」で実行可能な各種操作をAPIとして提供します。
- 画面間の遷移は、ページクラスのAPIで表現されます。
- テストケースはページオブジェクトを使用して、UIを操作したり表示内容を読み取ったりします。
ページ オブジェクト層を用意することで、以下の恩恵が得られます。
- デザイン変更や文言変更によるHTML構造の内部的な変化は、ページオブジェクト層のクラスで吸収できる。
- 機能の追加/削除など仕様の変更による影響はページクラスのAPI変更となるため、コンパイラで(ある程度)検知できる。
- 共通ウィジェットへのアクセス手段を共通化できる。
- 内部実装の詳細を知らない人でも、テストの記述が可能。
/** ユーザー追加のテスト */ @Test public void testCreateUser() { UserManagementPage page = UserManagementPage.open(); // ユーザー管理ページを開く CreateDialog dialog = page.add(); // ユーザー作成ダイアログを表示 dialog.setLoginName("foo"); // ユーザー名を設定 dialog.setDisplayName("ふー"); ... dialog.ok(); // ダイアログを確定 // 画面のユーザーテーブルから一覧を取得して、ユーザーが追加されていることを確認 for ( User user : page.listUsers()) { if ( user.getLoginName().equals("foo") ) { return; } } Assert.fail(); }
テストごとのデータ初期化
格納されたデータの違いによってテスト結果に違いが出ないように、テスト実行前にデータの初期化を行います。
- 登録済みデータに依存しないテストにするという道もありますが、考慮事項がどうしても多くなる茨の道なので、テストごとの初期化を選択。
- 登録済みデータに依存しないテストにしておくと、稼動中の任意の環境に対してテストが実行できるメリットもあります。このへんの要件は今回は切りました。
- テスト内で作成したデータの後始末も、考慮しなくて良くなります。
初期化は、テストケースからJDBCでDBに直接アクセスして実行します。テスト内で大量のデータが必要な場合も、ドメイン層のAPIをテストケースで叩いて登録します。
Selenium Grid での分散実行
Selenium Gridを使用してテストを複数マシンで分散実行できるようにしました。
SeleniumでのUI自動テストは、ブラウザの操作やUI処理の待ち合わせでどうしても時間がかかります。1ブラウザで一通りのテストを実行するだけで4〜5時間は必要で、さらにそれをサポートブラウザ一式で実行する必要があるため、逐次実行だと1日でテストか完了しません。
そこで、Selenium Gridの出番です。
- Gridにクライアントノードを登録しておけば、
- テスト実行時に、Gridが空きノードを割り当てて分散実行してくれます。
- 登録ノードを増やせば、それに応じて実行時間のスケールが可能。
問題はDBの初期化処理です。並列実行中に他のテストから初期化された!などとなってはまずい。これについては、DBも複数用意することで対処しました。
-
- 接続先サーバー(接続先DBとアプリケーションURL)のペアをテストケース側で管理し、
- 各テストごとに接続先サーバーを割り当て、テスト実行中は占有されるように制御します。
全体の構成は以下のようになります。
テストの並列化はmaven-surefire-pluginの並列実行機能を利用しました。当初はカスタムのテストランナを作成して実施していましたが、これだとテスト結果が稀に抜け落ちる問題(maven-surefire-pluginのテスト集計機能がスレッドセーフではない模様)があったため、ご注意。
感想
想像を絶する実装コスト
自動テストの作成は、思いのほか工数がかかります。主な原因は次の2つですね。
- テスト実行の所要時間
- 「実行→動作しなかった箇所を修正」のサイクルがどうしても長くなってしまい、効率が上がりません。
- 時間を食う原因がブラウザの起動/操作時間であるため、改善は原理的に無理かと。
- テストを安定させるのが大変
- UIの自動テストでは、実行時のネットワーク状態とかサーバーの負荷の問題で、「タイミングにより稀に失敗する」箇所が多数発生します。
- 対処として、待ち合わせ処理またはWaitを入れることになりますが、
- 発生が稀であるため問題箇所の特定が難しく、「実行してみて発生したら潰す」作業を延々繰り返す羽目になります。
- 疑わしい箇所に長めのWaitを入れちまう手もありますが、それはそれでテスト実行の所要時間が長くなってしまう問題があり・・。
ページオブジェクトの再利用とかコード重複の排除は比較的徹底して行いましたが、↑の2つは原理的に打つ手が見つかりません。ざっくりとてすが、テスト作成の工数はWebアプリ本体の実装工数の50%くらいは必要な印象です。
UI表示のテストは苦手
Seleniumは、ブラウザを操作してのWeb UIの機能試験は得意ですが、UIが期待通り表示されているかのチェックは苦手です。
- DOM上に○○というエレメントが存在する。
- エレメントのdisplayがnoneでは無い。
というレベルであれば何とかなりますが、以下を確認するのは厳しいです。
- ○○というエレメントが座標XXにある。
- エレメント内のテキストが枠からはみ出していない/途中で切れたりしていない。
スクリーンキャプチャを取得することは可能なので、画像を解析して・・・というのも不可能ではないですが、実装・保守が大変でしょう。(デザインの微調整などは多々ありますし。)
なので、残念ながらUI表示については目視で確認することになります。「UI自動テストを用意しておけば、サポートブラウザを増やしてもスケールしますよ!!」とは正直言えません。さらに、ブラウザの違いにより問題が起きやすいのはUI表示の部分ですよね・・・というジレンマもあります。
ただし、以下のような使い方は可能だと思います。
無駄なテストは作らない
UI自動テストに関しては、実行時間/安定性/メンテナンスコストの観点で、不要なテストは極力減らすことが重要です。
- テストが多いと、実行に時間がかかります。
- テストケース修正時の確認時間も延びるので、メンテナンスコストも増えます。
- テストが多いと、「タイミングの問題で稀に失敗する」可能性が高まります。
テストを書いていると、ついつい単体テストで確認済みの内容もテストに追加してしまったりします。Seleniumを使うことでよりUI操作に近い動作を確認できるという部分もあるし、基本的には「どんなテストも無いよりはまし」だとは思うのですが、ことUI自動テストに関しては意識的に無駄を排除しなければ破綻します。
ということで、以下の点にも注意するべきでした。
また、そもそもUI自動テスト,単体テストおよび手作業でのテストそれぞれのスコープをちゃんと整理できていなかった、というのも反省としてあります。テスト作成の前に、UI自動テストで何をどこまで保証するのかちゃんと定めておく、というのも大事かもです。
WebDriverはちょい不安定
WebDriverになって内部の仕組みが大きく変わり、できることは増えた(IEでのファイルアップロードがサポートされたりしてます)のですが、その分やや不安定になった印象です。テストを実行しているとSelenium WebDriver Serverが突然落ちる、といったことがあったりします。このため、Gridの維持には思いのほか時間を割かれました。
Selenium自体の改善を待つ以外には、以下のような対策があるかと思いますが、
- サーバーが落ちてもテストを継続できるように、各ブラウザごとに2,3台のクライアントノードを用意する。
- 稼動監視機構を用意して、落ちたら再起動するようにする。
残念ながら、そこまでする工数や機材はありませんでした。
まとめ
- テストの保守性には気を配る。
- ページオブジェクトパターンを採用する。
- 動作保証は、なるべく単体テストで。
- 自動テストのスコープを決め、無駄なテストは作らない。
- UI自動テストの作成にはかなりの工数がかかることを考慮する。
- 見積もり工数にあらかじめ入れておくとか、選任の担当者を用意するとか。
- ページオブジェクトは開発が作成し、テストケースの方はアルバイトさんとか品質保証チームに作ってもらう、という分担も手です。
- テストを作成してそれがちゃんとペイするかどうかの見極めも必要です。
- 単体テストのセオリーが適用できないところもあります。
- 全テストの実行に数時間とかかかるので、「コミット前にすべてのテストが通るのを確認すること」などというルールは守れません。
- 100%の安定性はあきらめて工数の増大を抑える、というのも選択としてはありです。
- テストの一般的なセオリーを無理に適用するのはやめ、効果と工数のバランスをみながら判断しましょう。
- Selenium Gridは社内インフラとして整備するのがよいのでは。
短くなるリリースサイクル、サポートブラウザ/OSの増加、 案件ごとにカスタマイズなどのため、動作保証にかかるコストはだんだんと増加しています。「工数が無いのでサポートブラウザ減らします」とか「カスタマイズには対応できません」というのも仕方がない部分はありますが、開発者として自動化できる部分はなるべく自動化し「その辺は見越してスケールするようになってますよ!! キリッ」とか言えるようになりたいものですな。