🌟

アプリのテストの自動化が困難な理由と、テストツールを自作した話

2023/08/29に公開

アプリのテストの自動化は困難な道

現在筆者はアプリの品質保証に関わる人なので、アプリのテストに関する管理や実行を行うことを仕事としています。
自動化による業務効率化も主なタスクの一つです。

具体的にはExcelマクロによるテスト結果の集計・表やグラフの生成、スタブツールの提供、自動テストツールの提供・テストコード作成・自動テスト環境の維持、通信ログ異常検知ツールの提供など。チームのマネージメントを行う傍ら、テストに関わるタスクの効率化や、ツールの提供を行なっています。

意図して目指したわけではありませんが、テストに関する諸問題をソフトウェアエンジニアリングで解決しているので、最近の流行りで言うところのいわゆるSET(Software Engineer in Test)なのだと思います。

そんな筆者の視点から見ても、E2Eテスト、すなわちアプリのテストの自動化は困難な道のりです。

巷でよく言われているように、スモールスタートで実現可能な範囲でとりあえず始めましょうと言うのは簡単でも
その後、費用対効果で満足のいくレベルの十分なアウトプットを得るには膨大な努力と継続的なメンテナンスが必要です。

テスト自動化の導入プロジェクトの多くは失敗すると言われることがあります。当たらずしも遠からずだと思います。
ただ、そこで挙げられている理由については、テスト自動化の計画がよくなかったとか、最適なテストツールの理解と選択が必要だとか、進め方が悪かったような説明がされますが、筆者としては少し違和感を感じています。一理あるけれども、本当にそこが一番の問題なのか、そういうヒューマンな話以前にツール側で解決すべきことがあるのではないか、と感じています。

アプリの自動化に関するツールの課題

アプリのテストの自動化が困難であることについて、テクニカルな面で以下の課題を解決する必要があると筆者は考えます。

  1. 既存のテストツールが低水準なAPIしか提供しておらず、抽象度の高い機能が不足しており生産性が低いこと
  2. 採用したテストツールを前提としたテスト自動化の導入メソッドが未成熟であること
  3. 既存の品質管理(手動テスト)と自動テストの折り合いが悪いこと

自作ツールによる課題の解決

スマホアプリのテストの自動化に取り組むにあたり、筆者はOSS製品であるAppiumを使用することとし、足りないものはツールを自作するという方針で上記の解決を試みました。歳月を費やし少しずつ改良を重ねた結果、最近は十分実用的な完成度に達したと思っています。この成果は現在GitHubでShirates として公開しています。OSSなのでどなたでも無償でご利用いただけます。

アプリの自動化が困難な理由

本記事ではアプリの自動化が困難である理由(課題)を具体的に示します。また、Shiratesで解決できるものについては活用方法を紹介します。

  • 要素の取得(ロケーター)
  • テストコードの複数プラットフォーム対応
  • エラー原因調査の容易性
  • テスト結果レポートと自動テストのアカウンタビリティ
  • 画面遷移におけるイレギュラー
  • テストデータのセットアップ
  • 自動テストと手動テストの棲み分け
  • 実行速度の問題

要素の取得(ロケーター)

画面上の要素をどのように取得するかということは、テストの自動化において最も基本的なテーマです。

課題

一般的には開発時にあらかじめ要素にIDを埋め込んでおき、テスト実行時はIDで要素を取得する方法が最初に紹介されます。実際にやってみると、固定レイアウトの画面では上手くいきますが、複数のアイテムがスクロールで表示される画面においては上手くいきません。同じ項目が複数回出現するパターンにおいて、取得したい要素を狙って取得できるような都合の良いIDを付与することはできないからです。

人間がテストを行う場合はどうなのか考えてみましょう。人間は画面に表示されている情報で判断して画面を操作しています。各要素に裏で設定されているIDは見えないので意識しません。なので、IDではなく、目に見えるテキストで要素を特定しています。

ラベル要素についてはテキストが存在するのでそれで上手くいきますが、テキストが存在しない入力項目、ボタン、スイッチ、画像などについてはどう取得すれば良いでしょうか?

人間の場合、項目の座標やサイズなどの視覚情報から項目間の相対的な位置関係を識別し、互いの関連性を把握します。例えば、ラベルの右側に入力項目が配置されていれば、そのラベルに対応する入力項目だと解釈します。

リストの場合、関連性のある項目がグルーピングされてアイテムを構成し、複数回表示されていることを理解します。リスト内で目的のアイテムを操作する場合、まずは商品名のようなデータとしてユニークであるテキストを探してアイテムを特定し、次にそのアイテムを構成する要素から目的の要素を特定するという手順を踏みます。

このように、人間の認知プロセスと同じように座標情報を利用すれば、目的の要素を取得することができそうです。
しかしながら、Appiumには「ラベルの右側にある入力項目を取得する」というような便利な機能はありません。

Shiratesにおける解決

上記の課題を解決するために、 相対セレクター を実装しました。
相対セレクターを使用すると、以下のような相対的な項目取得が可能になります。

  • 氏名ラベルの右側の入力項目
  • 商品ラベルの右側のボタン
  • Wi-Fiラベルの右側のスイッチ

詳細はこちらの記事を参照ください。
https://zenn.dev/wave1008/articles/501da678760cf0

テストコードの複数プラットフォーム対応

課題

AppiumはAndroidアプリとiOSアプリを操作することができますが、同一仕様のアプリのテストコードが一本化できるわけではありません。AndroidとiOSでは画面上の要素の表現が異なるためです。これはAppium Inspectorで画面をキャプチャして要素の情報を確認してみればわかります。

同一仕様のアプリなのだから、同一のテストコードでテストしたいのが人情であり、それが実現できれば生産性が著しく向上します。このようなプラットフォームの差異はAppiumをラップしてドライバーとして使用しつつ、プラットフォーム共通で使える独自のAPIを定義することで解決できます。

Shiratesにおける解決

Android, iOSで共通的に要素取得を表現できるようにするため select関数セレクター式 を実装しました。

詳細はこちらの記事を参照ください。
https://wave-diary.hatenablog.com/entry/2022/10/16/020222

エラー原因調査の容易性

課題

テストコードはプログラムなので、正しく実装できるまでは繰り返しエラーが発生します。また、一旦完成したテストコードも、アプリや環境の変化によってエラーが発生することがあります。エラーの原因を把握するにはログが有効ですが、Appiumのログだけでは原因を把握することは困難です。エラーの原因を効率良く把握できるようなログ出力機能が必要です。

Shiratesにおける解決

ユーザーアクションに対応するログを自動で出力するように実装しました。

たとえばtap関数を実行した箇所は以下のように日本語でログが出力されます。

(tap)  [1]をタップする
(tap)  [2]をタップする
(tap)  [3]をタップする

また、textIs関数を実行した箇所は以下のように出力されます。

(textIs)  [式]の値が"123"であること

ログ出力の例

対応するテストコード

    @Test
    @DisplayName("123+456")
    fun s10() {

        scenario {
            case(1) {
                condition {
                    it.macro("[電卓を再起動する]")
                    it.screenIs("[電卓メイン画面]")
                }.action {
                    it
                        .tap("[1]")
                        .tap("[2]")
                        .tap("[3]")
                }.expectation {
                    it.select("[式]")
                        .textIs("123")
                }
            }
            case(2) {
                action {
                    it.tap("[+]")
                }.expectation {
                    it.select("[式]")
                        .textIs("123+")
                }
            }
            case(3) {
                action {
                    it
                        .tap("[4]")
                        .tap("[5]")
                        .tap("[6]")
                }.expectation {
                    it.select("[式]")
                        .textIs("123+456")
                    it.select("[計算結果プレビュー]")
                        .textIs("579")
                }
            }
            case(4) {
                action {
                    it.tap("[=]")
                }.expectation {
                    it.select("[計算結果]")
                        .textIs("579")
                }
            }
        }
    }

テスト結果レポートと自動テストのアカウンタビリティ

課題

テスト結果を報告する場合、どのような手順で、チェック箇所が何件で、OKが何件なのかということを説明する必要があります。Appiumにはそのようなレポートを作成する機能がありません。

Shiratesにおける解決

  • HTMLレポート
  • Spec-Report(Excelテスト仕様書)
    を実装しました。

詳細はこちらのドキュメントを参照ください。
クイックスタート

画面遷移におけるイレギュラー

スマホアプリはFirebaseやSalesforceの提供するアプリ内メッセージ機能を使用することで、画面遷移の途中に広告用のポップアップ画面を挿入することが容易にできます。特定の画面遷移をおこなった時に必ず挿入されるわけではなく、指定した条件次第で表示されたり、されなかったりします。

また、アプリ内メッセージではなくとも、初回利用時のライセンス合意や利用上の注意事項など、カスタムのポップアップ画面が挿入されるケースも多いです。

このようなイレギュラーなポップアップ画面をテストコードで適切に処理しようとすると、各ポップアップ画面に固有の要素が表示されているかを判定し、要素が存在する場合はダイアログを閉じる操作を実行するような条件分岐が必要になります。

このような条件分岐をテストコード上で都度記述すると、本来実施したいテストではない部分の記述の割合が多くなり可読性や保守性が低下します。したがって、イレギュラー処理を共通化することが必要になります。

Shiratesにおける解決

イレギュラーハンドラー を実装しました。
イレギュラー処理を1箇所にまとめて記述し、共通化することができます。

詳細はこちらの記事を参照ください。
https://wave-diary.hatenablog.com/entry/2023/03/18/010242

テストデータのセットアップ

課題

テストを実行するには事前条件をセットアップしなければなりません。サーバー環境について言えば、主にはデータのセットアップが必要となります。他者の作業との干渉を避けるため、本来であれば自動テスト用の専用の環境が必要です。しかし、そのような環境を用意し、維持するには多額の費用がかかるため、あまり期待はできません。

自動テストを安定実行するにはスタブを活用することが有力な選択肢となります。

Shiratesにおける解決

スタブツールとして shirates-stub を実装しました。汎用的なスタブ機能を実装したWebアプリです。
スタブデータの切り替えをAPIで行うことができるので、テストコードでAPIを呼び出してデータパターンを切り替えて使用できます。

自動テストと手動テストの棲み分け

課題

テストの自動化を行うにあたり、何を手動テストで行い、何を自動テストで行うのかの方針を決める必要があります。費用対効果を考慮すると、繰り返し実行されるテストを自動化すべきであり、まずはリグレッションテストが候補に上がります。リグレッションテストはアプリの一部を改修した影響で従来利用できていた機能が利用できなくなる不具合(デグレード)が埋め込まれるリスクを払拭するために、アプリの各機能が正常に利用できることを確認する包括的なテストです。

リグレッションテストが手動テスト用のテスト仕様書としてExcel等で作られている場合、その中のテストシナリオを順次自動化していくことになります。

ここで問題になるのは、手動で行っていたテストが全て自動化できるわけではないという現実です。自動化ができないテストとは、技術的に困難なもの、技術的には可能だが費用対効果が見合わないものを指します。

たとえば以下のようなものがあります。

  • アニメーションが適切に表示されることを確認する
  • バックライトの輝度が最大になることを確認する
  • 他要素認証としてSMSで送られてくるコードを入力する
  • CAPTCHAで表示される画像の文字を入力する
  • プッシュの着信音が鳴ったことを確認する

自動化したいテストシナリオの一部が自動化できない場合、テストシナリオを自動化できる部分とそうでない部分に分割して再構成すればこの問題は解決することができますが、テストシナリオは冗長になってしまいます。

Shiratesにおける解決

Shiratesでは manual関数 を使用することで手動で実施すべきテストをテストコード内に記述することができます。自動テストを実行すると Spec-Report(Exlcelテスト仕様書) に出力されるので、自動テストを実行した後に、追加で手動テストを実施し、結果を追記することができます。

実行速度の問題

課題

JUnitで行うようなクラス・メソッド単位の単体テストは大部分がメモリ内で実行されるため非常に高速です。一方、Appiumを使用したE2Eテストはプロセス間通信を行う上、そもそもユーザーインターフェースの処理が重く、アニメーションによる待ちや、外部サービスへの問い合わせがあるため、実行速度は非常に遅いです。このため、テストの自動化を進めていくうちに、実行時間が非常に長くなります。テスト自動化を推進していくと、一晩かけても自動テストが終わらないという事態になりかねません。本格的にテスト自動化を推進していけば、実行時間の短縮は避けては通れない課題となります。

テストコードをチューニングすることで高速化できる場合がありますが、限界があります。さらに高速化するにはテストを並列実行する必要があります。

並列実行するには端末のセットアップや通信ポート番号の設定など、手間がかかります。物理端末を並列化で利用するとOSのバージョンや画面解像度などの条件をそろえるのが困難なので、エミュレーター/シミュレーターを使用した方がよいと思います。VMのインスタンスの作成等、セットアップが自動化できるので管理も楽です。

テストコード上のテストシナリオに順序性がある場合、それらを別々の端末で並列実行してしまうとテストが正常に実行されないので、そのような順序性がないようにテストシナリオを作成するか、順序性がある一連のテストシナリオが同一の端末で連続実行されるようにテストシナリオを整理し、並列実行をプラニングする必要があります。

Shiratesにおける解決

並列実行をサポートする機能はShiratesとしては提供していませんが、以下の方法で実現することが可能です。

ハードウェア

  • Mac Studioのスペック全部盛りを調達する(M2 Ultra, 24コア, 192GBメモリ)

テスト端末

  • Mac Studio内でエミュレーター/シミュレーターのインスタンスを複数定義する

並列化をセットアップするシェルスクリプト

  • テストプロジェクトのフォルダを並列化する分だけ複数コピーする
  • コピーしたプロジェクトのフォルダにテストを実行するシェルスクリプトを作成する
  • テストを実行するシェルスクリプトを起動する
  • テストを実行するシェルスクリプトの実行が全て完了したら、テスト結果をマージするシェルスクリプトを起動する

テストを実行するシェルスクリプト

  • GradleでJUnitを起動する。Shiratesのテストコードが実行される

テスト結果をマージするスクリプト

  • 集約されたテスト結果をマージして集計する

どれくらい並列化できるかについては、頑張ったら10並列くらいいけます。
※オーバーヘッドがあるので10倍速くなるわけではありません。

まとめ

本記事ではアプリのテストの自動化が困難な理由とその具体例をいくつか示しました。
また、自作ツールによってその困難を乗り越えたことを紹介しました。

この記事が参考になったと思われた方はLikeのクリックをお願いします。

また、Shiratesがよいと思われた方はGitHubでStarをつけていただけると励みになります。

https://github.com/ldi-github/shirates-core

Discussion