🤖

Android / iOSアプリのE2Eテスト全部で250シナリオを自動化しました! 第3回 壊れやすいテストと向き合う

2023/03/28に公開

こんにちは。
株式会社ココナラで技術戦略室を担当しているKと申します。

ココナラアプリのE2Eテストシリーズ第3回目です。

今回は、E2Eテストの壊れやすさとその対応についてお話ししたいと思います。
まだ第1回、2回をご覧になっていない方は、先に以下のリンクからご覧いただければ幸いです。

https://zenn.dev/coconala/articles/a3a5e33cd1d981
https://zenn.dev/coconala/articles/a3a5e33cd1d982
第3回. E2Eテストの壊れやすさにどう対応しているのか ←今回

要約

本記事を要約すると、以下のようになります。

  • E2Eテストは脆いです。ちょっとしたことで壊れます。
  • ココナラでは、壊れやすさに対して、現在以下の3つの対応を行っています。
    • ✅ 大きく壊れる前に気付けるようにする
    • ✅ 壊れた原因を特定しやすくする
    • ✅ 直しやすさを追求する

以降、それぞれの詳細をお話しします。

E2Eテストは脆い

E2Eテストは脆いです。ちょっとしたことで壊れます。
その最大の理由は、テストコードに内部構造が書かれる点にあると考えています。

これは、ユニットテストと比較してみると分かりやすいです。

ユニットテストの場合

ユニットテストの場合

関数/メソッドの内部をどれだけリファクタリングしても、出力さえ正しければテストは壊れません。

E2Eテストの場合

E2Eテストの場合

画面を操作するために、本来テスト対象ではない内部構造(UIコンポーネントのID、パスなど)がテストコードに書かれます。
そのため、リファクタリングなどにより内部構造が変わると動かなくなってしまいます。

E2Eテストの壊れやすさにどう対応するか

ココナラでは以下の3つの対応を行っています。

  1. 大量に壊れる前に気付けるようにする
  2. 壊れた原因を特定しやすくする
  3. 直しやすさを追求する

✅ 1. 大量に壊れる前に気付けるようにする

E2Eテストが大量に壊れると直すのが大変です。(時間的にも気持ち的にも)
いざ、テストを実行しようとしたら、失敗が多発して愕然とするかもしれません。

放置

そうならないように、ココナラでは以下の運用を行っています。

  • 毎日夜間にE2Eテストの全シナリオを実行しています。
  • テスト結果がSlackに通知されます。
  • 失敗したテストがあれば翌営業日に確認して直します。

Slack
1件失敗した図

修理
壊れに気づいたらすぐに直します

✅ 2. 壊れた原因を特定しやすくする

2つめは、壊れた原因を特定しやすくするということです。

E2Eテストは原因特定が難しい

以下のように、E2Eテストが失敗した場合、ユニットテストとは違って原因がすぐには分からないことが多いです。

  • ユニットテストの場合
    • 期待結果と異なっている
    • 例外が発生して失敗している

ユニットテストの場合

  • E2Eテストの場合
    • なぜだか分からないけど、途中で操作が失敗している

E2Eテストの場合

原因の特定に役立つ情報を自動取得する

そこで、ココナラでは原因の特定に役立つ情報を自動取得する仕組みを作っています。
具体例を挙げます。

  • Slackに操作が失敗したコード位置が通知されます。

    Slack

  • ReportPortalで以下の情報を参照できます。

    • 自動スクリーンショット
      各操作時点で画面がどのような状態になっていたのかが視覚的に分かります。
      この自動スクリーンショットは前回ご紹介したJavaScriptのProxy機能で実装しています。

      ReportPortal

    • 操作動画
      失敗するまでの画面の動きが視覚的に分かります。

      ReportPortal

    • 画面状態XML
      テスト終了時点の画面状態(Page Source)を取得しています。
      Appiumでは要素が見つからないというエラーがよく発生します。
      そのため、このXMLが役に立つことが多いです。
      XML

✅ 3. 直しやすさを追求する

最後は、直しやすさを追求するということです。

最近は壊れたE2Eテストを自動修復するようなサービスがあります。
しかし、アプリには対応していなかったり、対応していても精度に問題があったりします。

そのため、ココナラでは壊れるものは仕方がないと割り切っています。
そして、壊れたものを少しでも直しやすくする仕組みを導入しています。

E2Eテストコードの書きづらさ

E2Eテストのコードは書きづらいです。
書きづらいコードを直すのは大変です。

  • 書きづらさの例
    • 書き方が直感的でない
    • AndroidとiOSで書き方が違っている
    • 画面の作りによって書き方を変える必要がある

コードサンプル(改善前)

実際のコードを例に挙げます。

  • 「テキスト」という文字を持つ画面項目をタップする操作
    テストしたい内容に対して、$の中が冗長です。
    しかも、AndroidとiOSで書き方が異なります。

    // Androidの場合
    await driver.$('android=new UiSelector().text("テキスト")').click();
    
    // iOSの場合
    await driver.$('-ios predicate string:label == "テキスト"').click();
    
  • OR条件でタップする操作
    このようにXPathなどを使わないとできません。

    await driver.$('xpath: //android.widget.CheckBox[@text="条件1"] | \
      //android.widget.TextView[@text="条件2"]').click();
    
  • 「テキスト」という文字を持つ画面項目までスクロールする操作
    iOSとAndroidで書き方が異なります。
    また、同じAndroidでも画面の作りによって使い分けが必要になります。

    // iOSの場合
    const elementId = await driver.$(
      '-ios predicate string:label == "テキスト"'
    ).elementId;
    
    await driver.execute('mobile: scroll', {
      elementId: elementId,
      toVisible: true,
    });
    
    // Androidの場合
    // こちらの方がシンプルですが、RecyclerViewで動的に画面を生成している場合は使えません。
    const scrollableElementId = await driver.$(
      'android=new UiScrollable(new UiSelector().scrollable(true))'
    ).elementId;
    
    await driver.execute('mobile: scroll', {
      elementId: scrollableElementId,
      strategy: '-android uiautomator',
      selector: 'text("テキスト")',
    });
    
    // Androidの場合
    // こちらはより複雑ですが、RecyclerViewを使っている画面でも動作します。
    const scrollableElementId = await driver.$(
      'android=new UiScrollable(new UiSelector().scrollable(true))'
    ).elementId;
    
    while (true) {
      await driver.execute('mobile: scrollGesture', {
        elementId:scrollableElementId,
        direction: 'down',
        percent: 100,
      });
      if (await driver.$('android=new UiSelector().text("テキスト")')
        .isDisplayed()) {
        break;
      }
    }
    

直感的に書けるようにする

上記のような書きづらいコードを直すのは大変です。
そこで、少しでも直感的に書けるように以下の2つの仕組みを導入しています。

  • byというユーティリティオブジェクトでAndroidとiOSの差異を吸収する
    (これはDetoxを参考にしました)
  • WebDriverIOのChainablePromiseElementに、よく使う操作をex〜メソッドとして追加する
    (JavaScriptのProxy機能で実装)

コードサンプル(改善後)

これらの仕組みによって、以下のようにコードが直感的かつシンプルに書けるようになります。

  • 「テキスト」という文字を持つ画面項目をタップする操作

    // Android、iOS共通
    await by.text("テキスト").click();
    
  • OR条件でタップする操作

    // Android、iOS共通
    await(await by.or(by.text("条件1"), by.text("条件2"))).click();
    
  • 「テキスト」という文字を持つ画面項目までスクロールする操作

    // Android、iOS共通
    await by.text('テキスト').exScrollIntoView();
    

TypeScriptによる入力補完

また、さらにコードを書きやすくするため、TypeScriptを使用して入力補完が効くようにしています。

vscode
vscode

3つの対応の結果

ココナラでは、以下の3つの対応を行うことで、壊れたテストを比較的短時間で修復できています。
完璧ではないもののそれなりに上手く対応できているのではないかと考えています。

✅ 1. 大きく壊れる前に気付けるようにする
✅ 2. 壊れた原因を特定しやすくする
✅ 3. 直しやすさを追求する

まとめ

本記事では、E2Eテストの壊れやすさに対するココナラの取り組みをご紹介しました。
本記事が同じようにE2Eテストの壊れやすさと日々戦われている方の目にふれ、その布石となれば幸いです。

実際にこの仕組みをご覧になりたい方は、ぜひ以下フォームよりお気軽にご連絡ください!
ココナラに少しでも興味が湧いたという方も大歓迎です。
入力時間は1、2分です。
https://open.talentio.com/r/1/c/coconala/pages/70417

ココナラのエンジニアについてもっと知りたい!という方はこちらをご確認ください。

https://coconala.co.jp/recruit/engineer

Discussion