この記事は、Selenium/Appium Advent Calendar 2016の18日目の記事です。17日目は、xshsakuさんのAppiumとSTFを使ったAndroid実機テストでした。
はじめに
Seleniumテストを運用する上で安定性は大事です。テストが不具合以外の原因で頻繁に失敗してしまうと、チームメンバーは段々テスト結果を気にかけなくなってしまいます。そうなってしまうと、いざ不具合でテストが落ちても気づかれずに後回しになってしまう恐れがあります。自動テストの目的が問題の早期発見であることを考えると、これは深刻な問題になります。
この記事では、Seleniumテストが不安定になりやすい要因とその対策を、自分の経験の中からまとめてみます。
環境
Seleniumテストは、テストコードを実行するマシンさえあればいい単体テストと異なり、様々な環境が必要となります。そのため、不安定なテストの原因となる部分も多いです。Seleniumテストには、大まかに分けると以下の3つの環境が必要になります。
- テスト実行環境
- ブラウザ実行環境
- テスト対象環境
テスト実行環境
テスト実行環境は、ユニットテストなどと同様にテストケースを実行するための環境です。基本的には、この部分が不安定になるということはあまりないでしょう。テスト数が増えてきて並列実行するようになってくると、マシンリソースの問題で不安定になることはあるかもしれません。十分なリソースが確保されているかは気をつけましょう。
ブラウザ実行環境
ブラウザ実行環境は、Selenium Serverなどが起動しており、テストコードからブラウザが起動される環境です。環境のOSがWindowsだったりするとWindows Updateによって再起動がかかったり、前のテストのIEのプロセスが残ったりしてテストが不安定になることがあります。自分がWindowsをブラウザ環境として使っていたときに突然のブルースクリーンを経験したこともあり、正直Windowsは可能な限り避けてLinuxを使いたいです。
Linuxでも前のテストのプロセスやなんらかのゴミが残ったりすることがありえるので、Dockerを使ってテストスイート実行前後で環境を新規作成&破棄してしまうのがおすすめです。公式のDockerイメージも存在しているので、DockerでFirefoxやChrome環境を用意することはそこまで大変ではないと思います。Docker自体も不安定な部分はありますが、実機またはVMをメンテナンスしていくことに比べればはるかに楽です。
どうしてもWindowsでテストしないといけないという場合には、TaaS(Test as a Service)を利用することをおすすめします。Sauce LabsやBrowserStackといったサービスを利用すれば環境を自前でメンテナンスする必要がなくなり、毎回クリーンな環境を利用することができるはずです。少し前に、株式会社プレイドさんがBrowserStackの運用についてブログ記事を書かれていました。
しかし、2016年12月現在ではChrome以外のブラウザはWebDriverの実装がまだ十分とは言えない部分があります。最新の状況を追いきれてないのでひょっとしたら問題ないかもしれませんが、そのあたりを考慮に入れた上でコンテキストに合わせた選択をしてください。
テスト対象環境
テスト対象環境は、テスト対象となるWebサイトやWebアプリケーションが動いている環境です。ここでは、WebサーバーやDBサーバーなどテスト対象が持つすべてのサービスを含めています。テスト対象環境も過去のテストの影響を受けないように、可能な限りテスト実行前後でクリーンな環境を用意できると安定します。
そして、テスト対象環境は可能な限り本番運用と近い環境で行うようにしましょう。そうすればデプロイスクリプトは本番運用と同じものを使いまわせるはずなので、テスト用に独自のスクリプトを管理する必要がなく、安定した環境構築手順にもつながるはずです。また、本番に近い環境が不安定になるのであれば、それはおそらく本番環境にも不安定な部分があるということになるので、デプロイメントテストの役割も果たすようになります。
テストコード
テストコード側にも不安定になる要因はいくつかあります。
非同期に描画される要素の取得
Seleniumテストで不安定になりがちなのは、非同期で描画される要素を取得する場合です。要素が描画される前に findElement
を呼んでしまうと ElementNotFoundException
のような例外が投げられてしまいます。これを避けるためには、 sleep
のような固定の待ち時間を入れてしまうと実行時間が長くなってしまうので WebDriverWait
のような逐次的に確認する仕組みを使うといいです(参考)。
これを毎回記述するのはめんどうなので、要素を待ってから探すようなラッパーメソッドを用意すると楽です。Selenideのようなラッパーライブラリなら標準でそのような仕組みが入っているので、自作せずに活用するのもいいと思います。
要素がheaderやfooterなどに覆われてしまう
headerやfooterのようにスクロールしても消えずに他の要素の上に存在するものがある場合、クリックしようとした要素を覆ってしまってテストを不安定にしてしまう場合があります。
対策としては、まずウィンドウサイズはテストの最初に固定しましょう(参考)。これで実行ごとに結果が変わるということはかなり防げます。そして、要素を取得する前にその要素がウィンドウの中央にくるようにスクロールさせると、headerやfooterといった端の要素には影響を受けなくなります。残念ながら、Seleniumの機能だけではこれを実現できないので、JavaScriptを実行する必要があります(参考)。やや複雑な処理になるので、これも毎回書くのではなくてラッパーメソッドなどに組み込むといいです。
依存関係のあるテスト
他のテストに依存しているテストは不安定になりやすいです。ただでさえSeleniumテストは不安定になりやすいので、依存関係が発生すると依存先のテストの不安定性も足されてしまうことになります。メンテナンス性という点でもテストケース修正の影響範囲がわかりにくくなってしまうので、基本的にテスト間に依存関係を持たせるのはおすすめしません。
依存関係をなくすためには、テストケースごとにDBなどの状態を初期状態に戻す必要があります。そして、テストAで作成されるデータをテストBで使いたいのであれば、テストA→テストBの順序で実行させるのではなく、テストBの最初でテストAのデータを作成する処理を入れます。このとき、DBの初期化やテストデータ作成に時間がかかりすぎてしまうとまた別の問題が発生してしまうので、高速にテストデータを用意するための仕組みが必要です。もちろん、事情によってはそういう仕組みが用意できずに依存関係があるテストに頼らなければならない場合もあると思うので、最終的にはコスト対効果のバランスで選択していくことになります。
テスト対象
テスト対象の実装が原因でSeleniumテストが不安定になることもあります。
操作が速すぎると不安定になる
Seleniumテストは人間よりはるかに高速にブラウザを操作していくため、通常の手動テストでは見つからないバグを見つけてしまうことがあります。例えば、なんらかの非同期リクエストが発生する操作をした後に、そのリクエストが完了する前に他の操作をするとJSエラーが発生するようなケースが考えられます。
Seleniumテストの操作を遅らせるという手もありますが、これはバグと思われるのでテスト対象側を直したほうがいいでしょう。
包括的な対策としてのリトライ
個別に不安定となる原因を追求して改善していくことはもちろん大事ですが、一方でどれだけ対策してもSeleniumテストを完璧に安定させることは難しいです。なので、不安定なテストが存在する前提の対策も実際の運用ではあったほうがいいです。そのために、テストの失敗を一定回数まで許容してリトライする仕組みを導入するのがおすすめです(参考)。
一方で、リトライに頼りすぎるとテスト対象の不具合が原因で不安定になっているテストを見逃してしまいます。なので、リトライしたことをどこかで記録するようにして、後から原因を調べるプロセスを用意することも大事です。
別のアプローチとしては、不安定なテストを見つけたらアノテーションを付けて、アノテーションのあるテストが失敗したときだけリトライするような仕組みも考えられます。
TL;DR
- 十分なマシンリソースを確保
- ブラウザ実行環境はLinux+DockerもしくはTaaS
- テスト対象環境はできるだけ本番運用に近くてクリーンな環境を用意する
- 要素が非同期に描画されるのを待ってから取得するためのラッパーメソッドを用意するもしくはラッパーライブラリを使用する
- できるかぎりテスト間の依存関係をなくす
- テスト対象が原因で不安定になっている場合は不具合として修正する
- リトライの仕組みを導入する
まとめ
思いつくかぎりSeleniumテストが不安定になる原因と対策を書いてみました。他にも、時間依存のテストとかネタはあるのですが、コンテキストへの依存度が高すぎるので今回は書きません。気が向いたら追記するかもしれません。
Seleniumテストを安定させて、チームメンバーに信用してもらえるようにしましょう。
次回は、arakawa_moriyukiさんです。よろしくお願いします。