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

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

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/

tkmnzm

March 03, 2021
Tweet

More Decks by tkmnzm

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. ● 田熊 希羽(タクマ ノゾミ)
    ● 品質管理部 SWETグループ所属
    ○ Pocochaシステム部兼務
    ○ 株式会社Mobility Technologies兼務出向
    ● Androidとテストが好き
    4

    View Slide

  5. 2. 3つのAndroidプロダクトへの導入
    3. 導入する中で直面した課題
    4. 導入でつまずかないために
    5

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  18. ● 実アプリへの忠実度
    ● 実行時間
    ● 保守コスト
    ● デバッグコスト
    テストピラミッド
    自動テストのバランス
    についての指針
    18

    View Slide

  19. ● 実アプリへの忠実度
    ● 実行時間
    ● 保守コスト
    ● デバッグコスト
    この部分に該当
    19

    View Slide

  20. ● Fundamentals of Testing
    ○ https://developer.android.com/training/testing/fun
    damentals
    20

    View Slide

  21. 21

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  25. @Test
    fun capture() {
    // 画面起動前にテストデータのセットアップを行う
    // テストしたい画面の起動
    val intent = Intent(context, RankingActivity::class.java)
    val scenario = launchActivity(intent )
    scenario.onActivity activity
    // スクリーンショットを取得・保存
    }
    }
    実機やEmulatorを使った
    Instrumentation Testとして
    実行する
    25

    View Slide

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

    View Slide

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

    View Slide

  28. ● GO
    ○ タクシー配車サービス
    ● 乗務員アプリ
    ○ GOのタクシー乗務員が利用するアプリ
    ● Pococha
    ○ ライブコミュニケーションサービス
    うまくいかなかったプロダク
    トもあわせて、直面した様々
    な課題を紹介していきます
    28

    View Slide

  29. 1. スクリーンショットテストについて
    2. 3つのAndroidプロダクトへの導入
    4. 導入でつまずかないために
    29

    View Slide

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

    View Slide

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

    View Slide

  32. 意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    スクリーンショットテストを
    定着させる
    ● 依存の差し替えをどうするか
    ● テストの結合範囲をどうするか
    ● モックライブラリMockkの罠
    ● Applicationクラスの障壁
    ● 画面起動のパターンに対応する
    テストしたい画面を任意の状
    態で起動できるようにする
    32

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  36. ● FragmentFactoryと
    InterceptingActivityFactoryを使えば、テスト
    時に起動するFragmentとActivityのインスタン
    スを差し替えることができる
    ● 詳細(DIライブラリ未導入の方は是非)
    ○ Android UIテストでActivityとFragmentにコンストラ
    クタインジェクションする (Qiita)
    36

    View Slide

  37. 37

    View Slide

  38. 38

    View Slide

  39. 結合範囲
    39

    View Slide

  40. GO
    Pococha
    40

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  44. ● すでにユニットテストで使用していたmockkの
    Instrumentation Testサポートを使用
    ○ mockk.io/ANDROID
    ● Android P以上ではinline mock機能により、
    finalクラスやobjectのスタブも可能

    44

    View Slide

  45. ● バイトコードに処理を差し込むことで機能を実現
    ○ Mock final and static methods on Android devices
    ● Androidの実機で動作させるとランダムにクラッ
    シュする上、クラッシュのログがでない
    ● Andorid11ではinline mock機能を使わない場合
    でも、inline mockのセットアップでクラッシュ
    45

    View Slide

  46. ● inline mockを利用しない
    ○ サブクラス化してモック生成する機能を利用
    ○ 依存がIF化されていない場合、DexOpenerや
    All-open compiler pluginを導入する必要有
    ● Android11でクラッシュする問題は1.10.6で修正
    される予定(1.10.0を利用すれば一時的に回避化)
    46

    View Slide

  47. 前提として、Instrumentation Testでは通常のアプ
    リと同様にApplicationクラスが起動される
    47

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  51. Fragment
    Activity
    Pager
    51

    View Slide

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

    View Slide

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

    View Slide

  54. val activityScenario = launchActivity(intent)
    activityScenario.onActivity { act : MyActivity ->
    val navController = Navigation.findNavController(act, R.id.nav)
    navController.navigate(..)
    }
    起動したActivityのインスタンスからpublicアクセス
    できるものはテストコードからもアクセス可能
    54

    View Slide

  55. 意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    スクリーンショットテストを
    定着させる
    ● 依存の差し替えをどうするか
    ● テストの結合範囲をどうするか
    ● モックライブラリMockkの罠
    ● Applicationクラスの障壁
    ● 画面起動のパターンに対応する
    テストしたい画面を任意の状
    態で起動できるようにする
    55

    View Slide

  56. スクリーンショットテストを
    定着させる
    意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    テストしたい画面を任意の状
    態で起動できるようにする
    スクリーンショットをとって
    みたら、実アプリと違う表示
    になってしまうことがある
    56

    View Slide

  57. スクリーンショットテストを
    定着させる
    意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    ● 非同期処理の待ち合わせ
    ● スクリーンショットAPIの罠
    ● FragmentScenarioの罠
    ● スクロールする画面に対応する
    テストしたい画面を任意の状
    態で起動できるようにする
    57

    View Slide

  58. 非同期処理は?
    テストで結合する範囲に
    バックグラウンドスレッ
    ドの起動がある?
    Dispatcherを差し替
    えできるようにする
    準備なしでOK
    58

    View Slide

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

    View Slide

  60. ● Dispatcherの差し替え実装例
    ○ DroidKaigi/conference-app-2019
    ■ CoroutinePlugin.kt
    ● DataBinding利用時
    ○ android/architecture-samples
    ■ DataBindingIdlingResource.kt
    60

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  65. ○ ✕ リフレクション
    を使用すれば可

    ○ ○ リフレクション
    を使用すれば可
    SurfaceView
    単体のみ可

    (画面全体のみ)
    ○ ○ ○
    65

    View Slide

  66. ● スクロールをしながらスクリーンショットを取得
    ○ スクリーンショット → スクロール → スク
    リーンショット...
    ● 全体をスクリーンショットできるように画面をリ
    サイズしたあとスクリーンショットを撮る
    66

    View Slide

  67. 自動でスクロール +
    スクリーンショット
    取得を行うEspressoの
    Actionを用意
    67

    View Slide

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

    View Slide

  69. スクリーンショットテストを
    定着させる
    意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    ● 非同期処理の待ち合わせ
    ● スクリーンショットAPIの罠
    ● FragmentScenarioの罠
    ● スクロールする画面に対応する
    テストしたい画面を任意の状
    態で起動できるようにする
    69

    View Slide

  70. スクリーンショットテストを
    定着させる
    意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    テストしたい画面を任意の状
    態で起動できるようにする
    Visual Regression Testで
    差分の誤検知をしないように
    70

    View Slide

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

    View Slide

  72. ● 時刻や電池残量など表示が動的に変わる
    ● 全画面のスクリーンショットを撮る際には含まれ
    てしまう
    72

    View Slide

  73. ● 開発者オプションのシステムUIデモモード
    ○ Demo Mode for the Android System UI
    ● テストコードから有効化するには(値は↑を参照)
    ○ UiAutomation#executeShellCommandで
    adbを実行し、デモモードを有効化
    ○ 必要なコマンドをBroadcastで発行
    73

    View Slide

  74. 時刻を10:00・電池残量を100に固定
    74

    View Slide

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

    View Slide

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

    View Slide

  77. 地図の中身はすべて非表示
    カスタムで実装している
    マーカーのみになった
    77

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  83. 意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    スクリーンショットテストを
    定着させる
    テストしたい画面を任意の状
    態で起動できるようにする
    ● スクリーンショットテストが
    流行らない
    現在力を入れて取組中
    83

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  88. 意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    スクリーンショットテストを
    定着させる
    ● 依存の差し替えができるか
    ● アーキテクチャにあわせて結合
    範囲を検討する
    ● モックライブラリ利用の可否
    ● Applicationクラスにテストの
    邪魔になりそうな処理はないか
    ● アプリの画面構成とどのような
    起動経路があるかを確認する
    テストしたい画面を任意の状
    態で起動できるようにする
    88

    View Slide

  89. スクリーンショットテストを
    定着させる
    意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    ● 利用している非同期処理の機構
    にあわせて待ち合わせの仕組み
    を実装する
    ● 画面の特性にあわせて、スク
    リーンショットAPIを選択する
    ● FragmentScenarioの代替手段
    を用意する
    ● スクロールする画面の対応方針
    を決めて仕組みを実装する
    テストしたい画面を任意の状
    態で起動できるようにする
    89

    View Slide

  90. スクリーンショットテストを
    定着させる
    意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    ● 画面の中で動的に変わる箇所と
    固定化する方法を突き止める
    テストしたい画面を任意の状
    態で起動できるようにする
    90

    View Slide

  91. 意図した通りのスクリーン
    ショットを取得する
    安定したスクリーンショット
    を取得する
    スクリーンショットテストを
    定着させる
    ● スクリーンショットテストの
    メリットについてチームで合意
    ● 実装のハードルは都度改善
    テストしたい画面を任意の状
    態で起動できるようにする
    91

    View Slide

  92. ● UIテスタビリティを意識しつつ開発する
    ○ 画面実装時にも、テストで邪魔な処理が
    差し替えできるようになっているかを意識
    ● スクリーンショットテストでカバーしすぎようと
    しない
    ○ ユニットテストで見るべきでは?を考える
    92

    View Slide

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

    View Slide

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

    View Slide

  95. 95

    View Slide