Slide 1

Slide 1 text

Shared Test を実装する上で 気をつけなければいけないこと Cookpad.apk #4

Slide 2

Slide 2 text

自己紹介 ● 加藤 恭平(@ksfee684) ● モバイル基盤部所属 ● Android アプリプロジェクトの開発効率化、基盤技術開発

Slide 3

Slide 3 text

アジェンダ ● Android におけるテスト ● Robolectric はどのように機能するか ● Robolectric でできること/できないこと ● テスト実行環境の選択 ● Shared Test とは ● Shared Test を実装する上で気をつけること

Slide 4

Slide 4 text

今日伝えたいこと DroidKaigi で sumio_tym さんの 「Robolectric の限界を理解して UI テストを高速に実行しよう」をぜひ御覧ください

Slide 5

Slide 5 text

Android におけるテスト

Slide 6

Slide 6 text

https://developer.android.com/training/testing/fundamentals#testing-pyramid

Slide 7

Slide 7 text

テスト実行環境 ● 実機端末 or エミュレータ ○ Instrumentation Test ○ テストをパッケージングして端末上で実行 ○ 実行時間が長い ○ 複雑でメンテナンスコストが高く、デバッグが難しい ● ローカル JVM ○ クラス単位での Unit テスト ○ Android API を利用するテストがほぼ実行不可能 ○ 実行時間が短い ○ シンプルかつデバッグも比較的容易

Slide 8

Slide 8 text

なぜローカル JVM で Android API を利用できないのか ● クラス単体では OS の機能をベースとした動作を確認できない ○ Handler, Looper, UI(View レンダリング) ● ローカル JVM では以下のようなものがうまく動かない ○ イベントループ ○ ライフサイクルイベント ● ローカル JVM でもテストを実行したい

Slide 9

Slide 9 text

そこで Robolectric ● Android API を利用するテストをローカル JVM で動作させるすごいやつ ○ なぜかエミュレータを使わなくてもテストが動作する ○ なぜかローカル JVM で動作する ○ なぜか普通の JUnit テストより遅い

Slide 10

Slide 10 text

Robolectric は どのように機能するか

Slide 11

Slide 11 text

Android の OS の挙動を模倣している

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

動かない部分はごまかす(Shadow) ● 実際には実現できない部分、普通のローカル JVM でテストを実行する上で工夫しなければ 行けない部分を Shadow として実装 ● ほとんどのオブジェクトは Android API(SDK)の実装をベースとして一部のメソッドだけ置き 換えている

Slide 14

Slide 14 text

ローカル JVM で Android API を利用できるようになったので Activity や Fragment のテストもすべて Robolectric で完璧!!

Slide 15

Slide 15 text

すべてのテストを Robolectric で書けば良い? ● そんな夢みたいな話はない ● OS の挙動をあくまで模倣しているだけなので実際の動きとは異なるものになっている ● では具体的にはどういった点が違うのか

Slide 16

Slide 16 text

Robolectric で できること/できないこと

Slide 17

Slide 17 text

できること ● 結構いろいろできる ○ ライフサイクルメソッドの呼び出し ■ ActivityScenario, FragmentScenario ■ onCreate, onActivityResult ○ Context およびリソースへのアクセス ○ Espresso を用いた UI テスト ■ View へのアクセス

Slide 18

Slide 18 text

http://robolectric.org/androidx_test/ // GIVEN val contactName = "Test User" val scenario = launchActivity() // WHEN // Enter contact name onView(withId(R.id.contact_name_text)).perform(typeText(contactName)) // Destroy and recreate Activity scenario.recreate() // THEN // Check contact name was preserved. onView(withId(R.id.contact_name_text)).check(matches(withText(contactName)))

Slide 19

Slide 19 text

できないこと ● Shadow で実装されたメソッドで大きく Android 実行環境と異なる箇所がいくつか存在する ○ Looper のイベントループ ○ Activity 及び ActivityManager の状態管理 ○ View レンダリング

Slide 20

Slide 20 text

Looper ● 元々 Looper にポストされたタスクを同期的に実行する実装だった ● v4.3 からは PausedLooper という仕組みが入り、 Queue に一時的にタスクをため、任意のタイミング で execute が可能となった ● 参考 ○ http://robolectric.org/blog/2019/06/04/paused-looper/ ○ https://github.com/robolectric/robolectric/blob/master/robolectric/src/test/java/org/robolectric/shadows/ShadowLega cyLooperTest.java ○ https://github.com/robolectric/robolectric/blob/master/robolectric/src/test/java/org/robolectric/shadows/ShadowPaus edLooperTest.java

Slide 21

Slide 21 text

http://robolectric.org/blog/2019/06/04/paused-looper/ List events = new ArrayList<>(); events.add("before"); new Handler(Looper.getMainLooper()).post(() -> events.add("after")); events.add("between"); assertThat(events).containsExactly("before", "between", "after").inOrder();

Slide 22

Slide 22 text

http://robolectric.org/blog/2019/06/04/paused-looper/ List events = new ArrayList<>(); events.add("before"); new Handler(Looper.getMainLooper()).post(() -> events.add("after")); events.add("between"); // the 'after' task is posted, but has not been executed yet assertThat(events).containsExactly("before", "between").inOrder(); // execute all tasks posted to main looper shadowOf(getMainLooper()).idle(); assertThat(events).containsExactly("before", "between", "after").inOrder();

Slide 23

Slide 23 text

詳しくは… より Robolectric の詳しい内容は sumio_tym さんの 「Robolectric の限界を理解して UI テストを高速に実行しよう」をおすすめします

Slide 24

Slide 24 text

テスト実行環境の選択

Slide 25

Slide 25 text

Android 実行環境でのテスト実行がボトルネックに ● Android 実行環境構築のコスト ○ CI 環境の構築 ● 実行のコスト ○ ビルド -> パッケージング -> インストール -> 実行 ● とはいえ Robolectric で置き換えが可能か判断しにくいものもある

Slide 26

Slide 26 text

Shared Test とは

Slide 27

Slide 27 text

テスト実行タイミングによって実行環境を変える ● Robolectric と Android 実行環境どちらでも動作するようにテストを実装する ○ 実行頻度の高いタイミングでは Robolectric での実行結果で判断する ○ クリティカルなタイミングでは Android 実行環境化でテストを実行することでテスト結 果を判断する ● 日々の開発におけるテストの実行速度を高速化する ○ どちらの実行環境でも実行されるテスト -> Shared Test

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

Shared Test を実装する上で気をつけること ● Robolectric が理解できていないうちはすべて Android 実行環境上で動作させるべき ○ 下手にバグを踏まない ○ 実装をする上で考慮しなければならない事項を減らす ● UI テストの実行コストを減らしたい ○ テストごとに何を検証しているかを考える ○ ライフサイクル、非同期タスクの処理の順序等を考慮しなければならない場合はさける ● 実際のサンプルとかは? ○ CodeLab から消えている…

Slide 30

Slide 30 text

クックパッドでは? ● 現在のところクックパッドアプリでは Shared Test はありません! ○ 一部リソース(TestApplication 等)を共有するだけに留まっている ○ 今後テスト実行コストを評価した際に検討するかも …?

Slide 31

Slide 31 text

まとめ ● Android アプリにおける Robolectric の働き ● タイミングによって実行環境が変わる Shared Test ● Shared Test の実装目安