モバイルアプリのE2Eテストを運用して1年たった

男坂 Advent Calendar 2020 - Qiita 14日目。

仕事でモバイルアプリのE2Eテストを運用し始めて1年と少しが経過したので、テストコードの書き方、運用方法、感想、失敗したことを共有してみる。

背景

E2Eテスト導入前の状況

元々、開発対象のモバイルアプリにテストコードが一切なく、テストは全て手動で実施していた。また、正直、アプリの作りも雑でバグが多かった。

E2Eテストを導入した理由

手動でテストを実施するのは面倒だ。そして面倒だからこそ動作確認に手を抜いてしまいバグが多くなると、考えた。ということで、自動テストを追加して、それらの問題を解決しようという話になった。

普通であれば、Unit Testから充実させていく作戦を取ることが多いと思う。Unit Testの方が運用コストが低いからだ。テストピラミッドという考えでもUnit Testが自動テストの基礎でもある。

ただ、アプリのコードがテスト可能な実装になっておらず、Unit Testを書くためには大掛かりなリファクタリングが必須だった。そのため、まずは、E2Eテストで重要機能の動作を保証し、保証できた後にUnit Testを充実させる作戦にした。

E2Eのテストケースの選別

元々、手動で実施していたリリース前の動作確認項目をそのままテストケースにした。ただし、自動テストにしても安定性が低そうだったり、自動テストの実装難易度が高いものは自動化の対象外にした(プッシュ通知を開く、パスコードロック解除など)。

E2Eテストの書き方

AndroidiOSの両方でE2Eテストを導入した。

OS共通

テストコードにもデザインパターンがいろいろあるようだが、PageObjectパターンを採用した。テストコードを見たとき、どの画面のどの操作をしているのかが簡単に読み取れるようになるため。

テストフレームワークには、Espresso&XCTestを採用した。appiumのようなテストフレームワークを採用しなかったのは、プロダクト開発と自動テストを書くのが兼任だったため。プロダクト開発のノウハウがそのまま活かせるEspresso&XCTestの方が優位と判断した。

実行速度よりは、冪等性の担保、安定性、実行環境への依存性の少なさを重視した。テストコードで固定値sleepをするのは、実行速度にも安定性にも難があるので、極力避けた。

Android

Espressoを使ってテストコードを記述した。Android Studioから手動操作を記録してEspressoを使ったテストコードを生成できるのでそれを利用していた。生成したテストコードはそのまま使った訳ではなく、可読性や安定性が上がるように調整したり、端末依存性がなくなるように調整した。また、Page Objectパターンに当てはまっていないので、Page Objectパターンになるようにリファクタリングもした。

iOSと違い、手動操作からテストコードを生成する機能の完成度が高いので、テストケースを書くのは格段に楽だった。

各テストケース実行時には、Android Test Orchestratorで設定値を毎回クリアする設定にしていた。この設定のおかげで、テストケースの冪等性を担保しやすくなった。

通信などの非同期処理は、IdlingResourceで完了まで待機するようにしていた。IdlingResouceで処理を待機するのであれば、あまりWait処理はいらなくなるので。基本的には、非同期処理1つ1つに仕込みを入れるのではなく、RxであればRxJavaPlugins.setScheduleHandler、Coroutineであれば自作CoroutineContextを使うことで、全体的にIdlingResourceを設定していた。

iOS

XCTestで自動テストを実現した。Androidと違って、手動操作からのテストコード生成がまともにできなかったので、最初からコードをごりごり書いていた。

Androidと同じく各テストケース実行時には設定値を毎回クリアしていた。Android Test Orchestratorのような仕組みもないので、プロダクトコード側に独自の仕込みを入れて、設定値クリアを実現した。

IdlingResouceのような非同期処理の待ち合わせの仕組みはないので、タイムアウト値付きで特定の条件が整理するまでwaitさせる処理を自分で実装していた。

Androidよりもテストコードを実装する難易度が1, 2段高い。安定性の面でもAndroidよりも低い。

E2Eテストの運用方法

テスト実行環境

CIにて、特定のブランチにプッシュしたときや定刻になったときに、E2Eテストを実行させていた。Device FarmにはFirebase Test Labを採用した。採用理由は価格の安さ。

E2Eテストの実行パターン

主なE2Eテスト実行パターンは3種類にしていた(ブランチ戦略はGit Flow採用)。

  1. リリース前動作確認として
  2. 実行トリガーは、masterにpushされたとき
  3. 安定性が高いテストケースのみ
  4. ソフト構成は、なるべくRelease版に近いアプリ + 公開サーバ
  5. OSは最新OS、最新1つ前のOS、一番使われているOSを使用
  6. バイスは仮想デバイスと実機
  7. デグレチェックとして
  8. 実行トリガーは、developにpushされたとき
  9. 安定性が高いテストケースのみ
  10. ソフト構成は、Debug版に近いアプリ + 開発用サーバ
  11. OSは最新OS、最新1つ前のOS
  12. バイスは仮想デバイスのみ
    • 実行回数が多いのでコストを抑えるのが目的
  13. E2Eの各テストケースのヘルスチェックとして
  14. 実行トリガーは、毎日定刻になったら
  15. 全テストケースを実行
  16. その他の実行条件はデグレチェックのE2Eテストと同じ

特筆すべきなのは、3番目のヘルスチェックとしてのE2Eテスト。これは他の人にも自信を持っておすすめできる。これで各テストケースの安定性を見て、安定性が高いものだけをデグレチェックやリリース前の動作確認としてのE2Eテストに含めていた。こうすることで、E2Eテストが失敗したまま放置する状況を簡単に回避できる。また、E2Eのテストケースが失敗したときに、いつも失敗しているのか、偶発的に失敗したのかの判断を容易にする役割もある。

7~10日間、連続成功したら「安定している」と判断していた。一度安定していると判断したテストケースでも毎回失敗するようになったら、デグレチェックやリリース前の動作確認としてのE2Eテストの実行対象からは外していた。

追加したばかりのテストケースは、不安定なテストケース扱い。最初は、ヘルスチェックE2Eテストでしか実行させない。

E2Eテスト失敗時の扱い

通信やサーバ側起因の偶発的な失敗がかなり多い。それを踏まえて、E2Eテストが失敗したときは、以下のように対処していた。

  • 失敗のログなど見ないで、1回テストを再実行してPassするならそれでOKとする
  • E2Eテストが失敗しても、アプリのビルド&デプロイはする
  • E2Eテストを再実行して問題が起きる場合でも、手動で動作確認して問題ないならOKとする
  • テストケースの安定性が低いと判明した時点で、原因調査&対処 または リリース前動作確認やデグレチェックのE2Eテストからは外す

E2Eテストの感想

良かった点

きつい点

やはり運用コストが高い。

体感で月1くらいでE2Eテストが壊れる。要因はUIの変更、OSのバージョンアップ、ライブラリバージョンの変更、ビルド方法の変更など。

E2Eテストが壊れた原因が難解なこともしばしばある。まれにFirebase Test Labでipaのバンドル名が勝手に変更されてテスト失敗、しばしばAndroid 10から、WebViewの初期読み込みがかなり遅くなってテスト失敗など。

壊れる頻度 + 壊れた原因の調査の難易度が高いことから、プロダクト開発と並行してE2Eテストのメンテナンスはかなりきつかった。

Androidの方をメンテナンスするだけで精一杯で、iOSの方はメンテナンスしきれず、E2Eテストは壊れたまま放置した状態になってしまった。

べからず集

  • E2Eテストのメンテナンスを1人でやること
    • テストを壊す人は複数人にいるのに対して、修正する人が1人なのはきつい
    • その1人がメンテナンスやめたらすぐに廃れる
  • むやみやたらにE2Eのテストケースを作る
    • 単にテストケースを実装するのは楽だが、安定性を維持し続けるコストは高い
    • Unit Tesと違い、1つのテストケースの実行速度が遅いというのもある。テストのフィードバックは早く返すべき。遅いとテストを無視するようになる。
  • E2Eテスト失敗時に、アプリのビルド&デプロイをしないこと
    • アプリ外の要因でE2Eテストが失敗し続けることもあり、そういうときにビルド&デプロイしてくれないと、手動でビルド&デプロイする羽目になる
    • 誤検出も多いので、E2Eテスト失敗してもとりあえずデプロイまではして良い(何も調査しないでアプリを公開するのはNG)
  • E2Eテストの充実とプロダクト開発を兼任ですること(オーバーワークしない前提)
    • 既存のE2Eテストの維持 + プロダクト開発でもしんどい。E2Eテストの充実とプロダクト開発の兼任はなおさら無理。