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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. 結合範囲
    39

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    44

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  47. Fragment
    Activity
    Pager
    51

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

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

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide