Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題 / Android Screenshot Test Problems solved by introducing into 3 products

7867fe52a9be4257508a516d4df61578?s=47 tkmnzm
March 03, 2021

Android スクリーンショットテスト 3つのプロダクトに導入する中で倒してきた課題 / Android Screenshot Test Problems solved by introducing into 3 products

DeNA TechCon 2021での発表資料です
https://techcon.dena.com/2021/session/16/

7867fe52a9be4257508a516d4df61578?s=128

tkmnzm

March 03, 2021
Tweet

Transcript

  1. 品質管理部SWETグループ 田熊 希羽 1

  2. • Androidのプロダクトにスクリーンショットを 活用したテストを導入する中で、直面した課題と それらをどのように解決したか • 導入でつまずかないために検討するべきことと、 気をつけるべきこと 2

  3. 1. スクリーンショットテストについて 2. 3つのAndroidプロダクトへの導入 3. 導入する中で直面した課題 4. 導入でつまずかないために 3

  4. • 田熊 希羽(タクマ ノゾミ) • 品質管理部 SWETグループ所属 ◦ Pocochaシステム部兼務 ◦

    株式会社Mobility Technologies兼務出向 • Androidとテストが好き 4
  5. 2. 3つのAndroidプロダクトへの導入 3. 導入する中で直面した課題 4. 導入でつまずかないために 5

  6. • 手元で画面の表示確認をする • Visual Regression Test(画像回帰テスト) • カタログ化してデザインのレビューに利用 6

  7. • 手元で画面の表示確認をする • Visual Regression Test(画像回帰テスト) • カタログ化してデザインのレビューに利用 共通の目的はUIに関する不具合を早期発見すること 7

  8. • 手元で画面の表示確認をする • Visual Regression Test(画像回帰テスト) • カタログ化してデザインのレビューに利用 この発表ではこれらをひっくるめて スクリーンショットテストと表現します

    8
  9. • 画面実装時のデバッグ・動作確認の手段としてス クリーンショットを使用する • 画面によっては、アプリを操作して遷移したり、 条件によって変わる表示を再現するのが大変 ◦ テストで任意の画面や任意の条件を再現した スクリーンショットを取得できれば、動作確 認の時間を削減できる

    9
  10. • コードの変更前と変更後のスクリーンショット画 像を比較して差分を検知する • UIに意図しない変更が含まれていないかを確認で きる • ピクセル単位で比較することで、人の目で見て気 が付きづらいような差分も検知可能 10

  11. reg-suitを使った差分レポート 差分がある箇所が赤くなる 今回は文言を修正したこと による差分が検出されている 11

  12. • スクリーンショットを一覧化して確認できるよう にする • 開発者だけでなく、PdM・デザイナー・QAメン バーのレビューに利用できる • アプリを起動せずにUIを確認できる 12

  13. クレジットカードが無効 クレジットカードが期限切れ クレジットカードが有効 13

  14. • folio-sec/Fastfile screenshots-preview-generator.rb を参考に しつつカスタマイズ ◦ https://github.com/folio-sec/Fastfile/blob/master/ Scripts/screenshots-preview-generator.rb 14

  15. 1. スクリーンショットテストについて 3. 導入する中で直面した課題 4. 導入でつまずかないために 15

  16. • API通信といった外部依存はテスト用データを返 せるように(スタブ化)して、任意の状態再現が簡 単にできるようにする • 1テストケースのスコープを広げすぎない ◦ 例: 任意の状態で画面起動 +

    スクリーン ショットを撮ってテスト終了 16
  17. • API通信といった外部依存はテスト用データを返 せるように(スタブ化)して、任意の状態再現が簡 単にできるようにする • 1テストケースのスコープを広げすぎない ◦ 例: 任意の状態で画面起動 +

    スクリーン ショットを撮ってテスト終了 • テスト安定化のため • テスト実装のハードルを下げるため 17
  18. • 実アプリへの忠実度 • 実行時間 • 保守コスト • デバッグコスト テストピラミッド 自動テストのバランス

    についての指針 18
  19. • 実アプリへの忠実度 • 実行時間 • 保守コスト • デバッグコスト この部分に該当 19

  20. • Fundamentals of Testing ◦ https://developer.android.com/training/testing/fun damentals 20

  21. 21

  22. 22 モックライブラリなどで 実装を差し替える

  23. @Test fun capture() { // 画面起動前にテストデータのセットアップを行う // テストしたい画面の起動 val intent

    = Intent(context, RankingActivity::class.java) val scenario = launchActivity<RankingActivity>(intent ) scenario.onActivity activity // スクリーンショットを取得・保存 } } 23
  24. @Test fun capture() { // 画面起動前にテストデータのセットアップを行う // テストしたい画面の起動 val intent

    = Intent(context, RankingActivity::class.java) val scenario = launchActivity<RankingActivity>(intent ) scenario.onActivity activity // スクリーンショットを取得・保存 } } ActivityScenarioといった テスト用の画面起動APIが用意されている 24
  25. @Test fun capture() { // 画面起動前にテストデータのセットアップを行う // テストしたい画面の起動 val intent

    = Intent(context, RankingActivity::class.java) val scenario = launchActivity<RankingActivity>(intent ) scenario.onActivity activity // スクリーンショットを取得・保存 } } 実機やEmulatorを使った Instrumentation Testとして 実行する 25
  26. • GO ◦ タクシー配車サービス • 乗務員アプリ ◦ GOのタクシー乗務員が利用するアプリ • Pococha

    ◦ ライブコミュニケーションサービス 26
  27. • GO ◦ タクシー配車サービス • 乗務員アプリ ◦ GOのタクシー乗務員が利用するアプリ • Pococha

    ◦ ライブコミュニケーションサービス 実際に運用するのが難しくなり断念 27
  28. • GO ◦ タクシー配車サービス • 乗務員アプリ ◦ GOのタクシー乗務員が利用するアプリ • Pococha

    ◦ ライブコミュニケーションサービス うまくいかなかったプロダク トもあわせて、直面した様々 な課題を紹介していきます 28
  29. 1. スクリーンショットテストについて 2. 3つのAndroidプロダクトへの導入 4. 導入でつまずかないために 29

  30. テストしたい画面を任意の状 態で起動できるようにする 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる 導入までのハードルを 大きく4つに分割

    30
  31. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる テストしたい画面を任意の状 態で起動できるようにする 31

  32. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる • 依存の差し替えをどうするか • テストの結合範囲をどうするか

    • モックライブラリMockkの罠 • Applicationクラスの障壁 • 画面起動のパターンに対応する テストしたい画面を任意の状 態で起動できるようにする 32
  33. • DIライブラリを導入しているプロダクト ◦ ライブラリの仕組みを利用して差し替える • DIライブラリを導入していないプロダクト ◦ 依存先を解決する仕組みを自分で用意 ◦ デフォルト引数を駆使しつつ、コンストラク

    タでインスタンスを差し替え 33
  34. • DIライブラリを導入しているプロダクト ◦ ライブラリの仕組みを利用して差し替える • DIライブラリを導入していないプロダクト ◦ 依存先を解決する仕組みを自分で用意 ◦ デフォルト引数を駆使しつつ、コンストラク

    タでインスタンスを差し替え ライブラリによって都度調査して対応 今回導入したプロダクトではDagger2・koin 34
  35. • DIライブラリを導入しているプロダクト ◦ ライブラリの仕組みを利用して差し替える • DIライブラリを導入していないプロダクト ◦ 依存先を解決する仕組みを自分で用意 ◦ デフォルト引数を駆使しつつ、コンストラク

    タでインスタンスを差し替え プロダクトの1つはこれで対応 35
  36. • FragmentFactoryと InterceptingActivityFactoryを使えば、テスト 時に起動するFragmentとActivityのインスタン スを差し替えることができる • 詳細(DIライブラリ未導入の方は是非) ◦ Android UIテストでActivityとFragmentにコンストラ

    クタインジェクションする (Qiita) 36
  37. 37

  38. 38

  39. 結合範囲 39

  40. GO Pococha 40

  41. 変更 監視 乗務員アプリの ある画面の例(一部省略) 変更 監視 41

  42. 変更 監視 全体を結合しないと UIの変更が流れない 変更 監視 42

  43. • アプリのアーキテクチャによって結合範囲が変 わってくる • 結合範囲を広げると、UIが変化する条件を把握す るのが難しくなる • ユニットテストで担保する範囲を明確にし、スク リーンショットテストのスコープを調整する 43

  44. • すでにユニットテストで使用していたmockkの Instrumentation Testサポートを使用 ◦ mockk.io/ANDROID • Android P以上ではinline mock機能により、

    finalクラスやobjectのスタブも可能 ◦ 44
  45. • バイトコードに処理を差し込むことで機能を実現 ◦ Mock final and static methods on Android

    devices • Androidの実機で動作させるとランダムにクラッ シュする上、クラッシュのログがでない • Andorid11ではinline mock機能を使わない場合 でも、inline mockのセットアップでクラッシュ 45
  46. • inline mockを利用しない ◦ サブクラス化してモック生成する機能を利用 ◦ 依存がIF化されていない場合、DexOpenerや All-open compiler pluginを導入する必要有

    • Android11でクラッシュする問題は1.10.6で修正 される予定(1.10.0を利用すれば一時的に回避化) 46
  47. 前提として、Instrumentation Testでは通常のアプ リと同様にApplicationクラスが起動される 47

  48. • テストで困るApplicationクラスの実装例 ◦ 起動などをトリガーにAPI通信を行い、401エ ラーだったらログイン画面に遷移させる ◦ テストしたい画面の起動をブロックされる テストではテスト用Applicationクラスを使うことで 実行されないようにすることは可能だが... 48

  49. • プロダクトコード内で固有のApplcationを直接参 照していると切り離すのが難しい ◦ テスト用Applicationクラスに継承してもらう ことで回避は可能 ◦ その上で不要な処理を実行しないように改修 ▪ Applicationクラスが巨大だと骨が折れる

    49
  50. • スクリーンショットを取得したい範囲と起動方法 によって変わる ◦ Fragment単体 or 親Activity(Fragment)を含むか • 起動方法のパターン例 ◦

    PagerAdapterを利用している ◦ Navigation componentを利用している 50
  51. Fragment Activity Pager 51

  52. • 親画面も含んだスクリーンショットを撮る場合 は、親画面から起動する ◦ 子画面が参照している外部依存もあわせて差 し替えできるようにする ◦ 特にコンストラクタで差し替えをしている場 合は、親画面から渡せるようにする必要あり 52

  53. • ActivityScenario・FragamentScenarioを利用す ると起動しているActivity・Fragmentのインス タンスにアクセスできる • そこからNavigationControllerを取得し、 navigationを実行することで画面操作をせずに 遷移することが可能 53

  54. val activityScenario = launchActivity<MyActivity>(intent) activityScenario.onActivity { act : MyActivity ->

    val navController = Navigation.findNavController(act, R.id.nav) navController.navigate(..) } 起動したActivityのインスタンスからpublicアクセス できるものはテストコードからもアクセス可能 54
  55. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる • 依存の差し替えをどうするか • テストの結合範囲をどうするか

    • モックライブラリMockkの罠 • Applicationクラスの障壁 • 画面起動のパターンに対応する テストしたい画面を任意の状 態で起動できるようにする 55
  56. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する テストしたい画面を任意の状 態で起動できるようにする スクリーンショットをとって みたら、実アプリと違う表示

    になってしまうことがある 56
  57. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する • 非同期処理の待ち合わせ • スクリーンショットAPIの罠

    • FragmentScenarioの罠 • スクロールする画面に対応する テストしたい画面を任意の状 態で起動できるようにする 57
  58. 非同期処理は? テストで結合する範囲に バックグラウンドスレッ ドの起動がある? Dispatcherを差し替 えできるようにする 準備なしでOK 58

  59. 非同期処理は? テストで結合する範囲に バックグラウンドスレッ ドの起動がある? 準備なしでOK Repositoryでスレッド起動 かつ、Repositoryがスタブ 化されているケース 59

  60. • Dispatcherの差し替え実装例 ◦ DroidKaigi/conference-app-2019 ▪ CoroutinePlugin.kt • DataBinding利用時 ◦ android/architecture-samples

    ▪ DataBindingIdlingResource.kt 60
  61. • FragmentScencarioで内部的使われるActivity は、AppCompatActivityを • FragmentScenaioで起動した画面は、 AppCompatでのみ認識されるView属性が正常に 表示されない 61

  62. いない... app:srcCompatで 指定した VectorDrawable 62

  63. • FragmentScenarioの使用を避ける ◦ 親のActivityから起動する ◦ 空のAppCompatActivityにattachして Fragmentを起動できる仕組みを用意する ▪ 内部的にActivityScenarioを使った代替の FragmentScenarioなど

    63
  64. • Androidのテストで利用できるスクリーンショッ トのAPIは複数ある • APIによって、実際のアプリと見た目が異なる スクリーンショットがとれる場合がある • 詳細 ◦ Androidのテストで利用できるスクリーンショット取得API

    のまとめ (Qiita) 64
  65. ◦ ✕ リフレクション を使用すれば可 ✕ ◦ ◦ リフレクション を使用すれば可 SurfaceView

    単体のみ可 ✕ (画面全体のみ) ◦ ◦ ◦ 65
  66. • スクロールをしながらスクリーンショットを取得 ◦ スクリーンショット → スクロール → スク リーンショット... •

    全体をスクリーンショットできるように画面をリ サイズしたあとスクリーンショットを撮る 66
  67. 自動でスクロール + スクリーンショット 取得を行うEspressoの Actionを用意 67

  68. コンテンツのサイズに あわせてViewをリサイズ するオプションを追加 ただし UiDevice#takeSceenshot は端末で見える領域しか キャプチャしないため併用 できない 端末上でみえる 範囲

    68
  69. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する • 非同期処理の待ち合わせ • スクリーンショットAPIの罠

    • FragmentScenarioの罠 • スクロールする画面に対応する テストしたい画面を任意の状 態で起動できるようにする 69
  70. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する テストしたい画面を任意の状 態で起動できるようにする Visual Regression

    Testで 差分の誤検知をしないように 70
  71. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する • ステータスバーを固定化する • 地図の表示を固定化する

    • 時刻の表示を固定化する テストしたい画面を任意の状 態で起動できるようにする 71
  72. • 時刻や電池残量など表示が動的に変わる • 全画面のスクリーンショットを撮る際には含まれ てしまう 72

  73. • 開発者オプションのシステムUIデモモード ◦ Demo Mode for the Android System UI

    • テストコードから有効化するには(値は↑を参照) ◦ UiAutomation#executeShellCommandで adbを実行し、デモモードを有効化 ◦ 必要なコマンドをBroadcastで発行 73
  74. 時刻を10:00・電池残量を100に固定 74

  75. • 読み込み状態によって差分がでる • 中身が常に更新されうる 75

  76. • Google Mapsの場合は、道路や地名の表示等をオ プションで非表示にできる ◦ https://mapstyle.withgoogle.com/ • 一番安定するのは何も表示しない • カスタムの設定を作成し、テストコードから画面

    を起動した際にデフォルトの設定を上書きする 76
  77. 地図の中身はすべて非表示 カスタムで実装している マーカーのみになった 77

  78. • AndroidのUI widgetであるTextClockは、内部的 にCalendarのインスタンスを保持しており、 外から時刻を差し替えるのが難しい • 時刻フォーマットを(HH:mm等)指定するメソッド があるので、そこに時刻のフォーマットとして解 釈できない文字列を入れることで固定化可能 78

  79. activityScenario.onActivity { act -> act.findViewById<TextClock>(R.id.clock).format24Hour = "10:00" } findViewByIdで内部のViewにアクセスして状態を変更する 直接INVISIBLEにするといった力技も可能

    79
  80. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する • ステータスバーを固定化する • 地図の表示を固定化する

    • 時刻の表示を固定化する テストしたい画面を任意の状 態で起動できるようにする 80
  81. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる テストしたい画面を任意の状 態で起動できるようにする 81

  82. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる • スクリーンショットテストが 流行らない テストしたい画面を任意の状

    態で起動できるようにする 82
  83. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる テストしたい画面を任意の状 態で起動できるようにする • スクリーンショットテストが

    流行らない 現在力を入れて取組中 83
  84. • 自動テストにありがちな、テストを書く時間がな くて後回しになるという問題はスクリーンショッ トテストでも共通 • 効果が高いのは実装と一緒にテストを書くこと ◦ 少しでもテスト実装のハードルを下げられる ような取り組みを実施中 84

  85. • 画面起動やUI操作のヘルパーを実装してボイラー プレートを削減 • ペアプロやモブプロでスクリーンショットテスト の実装をサポート • スクリーンショット画像を端末から取得する Gradleタスクを用意 85

    etc...
  86. 1. スクリーンショットテストについて 2. 3つのAndroidプロダクトへの導入 3. 導入する中で直面した課題 86

  87. テストしたい画面を任意の状 態で起動できるようにする 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる 4つのポイントでつまづき そうな箇所がないかを確認

    87
  88. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる • 依存の差し替えができるか • アーキテクチャにあわせて結合

    範囲を検討する • モックライブラリ利用の可否 • Applicationクラスにテストの 邪魔になりそうな処理はないか • アプリの画面構成とどのような 起動経路があるかを確認する テストしたい画面を任意の状 態で起動できるようにする 88
  89. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する • 利用している非同期処理の機構 にあわせて待ち合わせの仕組み を実装する

    • 画面の特性にあわせて、スク リーンショットAPIを選択する • FragmentScenarioの代替手段 を用意する • スクロールする画面の対応方針 を決めて仕組みを実装する テストしたい画面を任意の状 態で起動できるようにする 89
  90. スクリーンショットテストを 定着させる 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する • 画面の中で動的に変わる箇所と 固定化する方法を突き止める テストしたい画面を任意の状

    態で起動できるようにする 90
  91. 意図した通りのスクリーン ショットを取得する 安定したスクリーンショット を取得する スクリーンショットテストを 定着させる • スクリーンショットテストの メリットについてチームで合意 •

    実装のハードルは都度改善 テストしたい画面を任意の状 態で起動できるようにする 91
  92. • UIテスタビリティを意識しつつ開発する ◦ 画面実装時にも、テストで邪魔な処理が 差し替えできるようになっているかを意識 • スクリーンショットテストでカバーしすぎようと しない ◦ ユニットテストで見るべきでは?を考える

    92
  93. 1. スクリーンショットテストについて 2. 3つのAndroidプロダクトへの導入 3. 導入する中で直面した課題 4. 導入でつまずかないために 93

  94. • スクリーンショットはUIに関する不具合の早期発 見に活用できる • Androidプロダクトに導入するには4つのポイン トでハードルがある • プロダクトの特性にあわせて、上記のポイントで ハマりどころがないか確認する 94

  95. 95