Slide 1

Slide 1 text

Androidテストハンズオン テストのないアプリにテストを書 こう編

Slide 2

Slide 2 text

今日の目標 ● テストが書きにくいアプリを改善する体験をしてもらい 実プロジェクトでトライする取っ掛かりにする 2

Slide 3

Slide 3 text

アジェンダ ● レガシーコードを改善しよう ● レガシーコード改善に役立つ知識 − テストダブル − レガシーコード改善テクニック ● テストのないアプリにテストを書こう 3

Slide 4

Slide 4 text

レガシーコードを改善しよう 4

Slide 5

Slide 5 text

社内テスト実施状況アンケートの結果 ● ユニットテストを「すでに導入しているが、もっと力をいれていき たい」「導入したいが、できていない」を合計して約75% ● ユニットテストにおける課題として、50%が「テストが書きにくい 設計になっている」と回答 5

Slide 6

Slide 6 text

テストが書きにくい設計になっている ● 新規に実装する − TDD等の手法を駆使しつつ、テストが書きやすい設計にする ● 既存のコードはどうする? − ごっそり書き直す VS 地道に改善を重ねる − ごっそり書き直すチャンスを得られるのは稀。そして成功する保証はない。 6

Slide 7

Slide 7 text

地道に改善を重ねる ● レガシーコード改善のすすめ − レガシーコードとは、単にテストのないコード(レガシーコード改善ガイドより) − テストを実装しながらテストが書きにくい設計を改善していく ● 改善をしないとどうなる? − レガシーコードに変更を加えていくことで、自分自身もレガシーコードを積み上 げてしまう悲劇 7

Slide 8

Slide 8 text

レガシーコード改善のアプローチ ● 保護して変更する − 変更する箇所をテストで保護をした上で安全にリファクタリング − ユニットテストは開発中に素早くフィードバックを得られる − このアプローチが辛い場合、手動での動作確認も代替案として有効 8

Slide 9

Slide 9 text

レガシーコード改善手順 1. 変更点を洗い出す 2. テストを書く場所を見つける 3. 依存関係を排除する 4. テストを書く 5. 変更とリファクタリングを行う 9

Slide 10

Slide 10 text

レガシーコード改善に役立つ知識 10

Slide 11

Slide 11 text

引用・参考について前置き ● この章の内容・ソースコードは下記本の内容を引用・参考にし て作成しています。 − 『Androidテスト全書』 − 著者: 白山 文彦, 外山 純生, 平田 敏之, 菊池 紘, 堀江 亮介 − https://peaks.cc/books/android_testing − 『レガシーコード改善ガイド』 − 著者: マイケル・C.フェザーズ 11

Slide 12

Slide 12 text

テストダブル ● テスト対象が依存しているコンポーネントを本物そっくりに振る 舞う代役(ダブル)と差し替えることで、自分の期待する挙動や 値の返却を達成する ● レガシーコード改善の中では、テスト対象が依存しているクラ ス・コントロールしたいクラスをテストダブルに置き換えながら テストを整備していく 12

Slide 13

Slide 13 text

テストダブル ● 5つのパターン − スタブ − モック − スパイ − フェイク − ダミー 13

Slide 14

Slide 14 text

スタブ ● 事前に定義した任意の値をテスト対象に与える ● 依存コンポーネントをスタブに置き換えることで、テスト対象に 任意の値を渡すことができる 14

Slide 15

Slide 15 text

スタブ実装例(テスト対象コード) public class WeatherForecast { private Sattelite satellite; boolean shouldBringUmbrella() { Weather weather = satellite.getWeather(); switch (weather) { case RAINY: return true; default: return false; } } } 15 satellite.getWeatherがRAINYのときのみ trueを返すメソッドをテストする

Slide 16

Slide 16 text

Libraryを用いたスタブの作成 Mockito/mockKともにモック・スパイオブジェクトがstubの機能を備えている Mockito(Java) Sattelite mock = mock(Sattelite.class); when(mock.getWeather()).thenReturn(Weather.RAINY); WeatherForecast testTarget = new WeatherForecast(mock); boolean result = testTarget.shouldBringUmbrella(); assertThat(result).isEqualTo(true); 16 任意の値を返却する設定

Slide 17

Slide 17 text

スタブの機能を実装する open class Satellite { open fun getWeather(): Weather { /* 元々の実装 */ } } /* 任意のWeatherを返すことができるスタブ */ class StubSatellite(val anyWeather: Weather) : Satellite() { override fun getWeather(): Weather { return anyWeather } } 17 任意の値を渡す 設定した任意の値を 返却

Slide 18

Slide 18 text

モック 18 ● テスト対象が依存コンポーネントに与える値や挙動(出力)を検 証する

Slide 19

Slide 19 text

モック実装例(テスト対象コード) public class WeatherForecast { private WeatherRecorder recorder; void recordCurrentWeather(Weather weather) { recorder.record(weather); } } 19 内部でWeatherRecorder#recordを呼び出し ているメソッドをテストする

Slide 20

Slide 20 text

Mockito(Java) WeatherRecorder mock = mock(WeatherRecorder.class); WeatherForecast testTarget = new WeatherForecast(mock); testTarget.recordCurrentWeather(Weather.SUNNY); verify(mock).record(Weather.SUNNY); Libraryを用いたモックの作成 20 メソッドが呼び出されたかと 与えられた値の検証

Slide 21

Slide 21 text

モックの機能を実装する 21 open class WeatherRecorder { open fun record(weather: Weather) {} } class MockWeatherRecorder : WeatherRecorder() { var weather: Weather? = null var isCalled = false override fun record(weather: Weather) { this.weather = weather isCalled = true } } メソッドの呼び出しと 与えられた値を記録

Slide 22

Slide 22 text

スパイ 22 ● テスト対象が依存コンポーネントに与える値や挙動(出力)を記 録する ● モックライブラリで提供されているスパイは、 実際にメソッドコールを行う振る舞いをすることが多い

Slide 23

Slide 23 text

スパイ実装例(テスト対象コード) public class WeatherForecast { private WeatherRecorder recorder; void recordCurrentWeather(Weather weather) { recorder.record(weather); } } 23 モックの際と同様のメソッドをテスト

Slide 24

Slide 24 text

Libraryを用いたスパイの作成 Mockito(Java) WeatherRecorder spy = spy(new WeatherRecorder()); WeatherForecast testTarget = new WeatherForecast(mock); testTarget.recordCurrentWeather(Weather.SUNNY); verify(mock).record(Weather.SUNNY); 24 クラスのインスタンスを 渡して生成する

Slide 25

Slide 25 text

スパイの機能を実装する 25 open class WeatherRecorder { open fun record(weather: Weather) {} } class SpyWeatherRecorder : WeatherRecorder() { var weather: Weather? = null var isCalled = false override fun record(weather: Weather) { this.weather = weather isCalled = true super.record(weather) } } 実クラスのメソッドを 呼び出し

Slide 26

Slide 26 text

[補足] xUnit Test Patternsでの定義 ● 現在のモックライブラリで提供されている機能とxUnit Test Patternsでの定義とは全く一緒ではない場合がある − テストダブルはx Unit Test Patternsで定義された用語 − 参考リンク − https://www.amazon.co.jp/dp/B004X1D36K/ − http://xunitpatterns.com/Test%20Double.html − http://goyoki.hatenablog.com/entry/20120301/1330608789 26

Slide 27

Slide 27 text

フェイク・ダミー 27 ● フェイク − 実際のコンポーネントと同等かそれに極めて近い挙動を持つ実装オブジェクト − 例: RoomのInMemoryDatabase ● ダミー − テストの結果に影響を与えないが、テスト対象クラスの生成・メソッドの呼び出 しに使用する代替オブジェクト

Slide 28

Slide 28 text

テストダブルの使いどころ 28 テストしたいクラス 依存しているクラス APIとの通信で実行時間がかかる など、いやな副作用がある 特定の振る舞いをテストしたいが、 再現するのが大変 依存しているクラスのメソッドが正し い引数で呼ばれているかわからな い 参照

Slide 29

Slide 29 text

テストダブルの使いどころ 29 テストしたいクラス 依存しているクラス このクラスをテストダブルに 置き換えてテストする

Slide 30

Slide 30 text

テストダブルの使いどころ 30 テストしたいクラス 依存している クラスのテストダブル 特定の振る舞いを再現する値を返 すようにする メソッド呼び出し時の引数の値を記 録して検証できるようにする APIとの通信など副作用がある処 理を実行しない

Slide 31

Slide 31 text

レガシーコード改善テクニック 31

Slide 32

Slide 32 text

レガシーコードのジレンマ 32 ● コードを変更するためには、テストを整備する必要がある。多く の場合、テストを整備するためには、コードを変更する必要が ある。 − レガシーコード改善ガイドでは、最低限のリファクタリングでテストを書けるよう にするテクニックを紹介している

Slide 33

Slide 33 text

テストの書きにくいコードが抱える課題 33 ● テストしたいコードがAPI通信などテストコードで 実行したくない機能に依存している ● テストしたいコードがグローバルなSingletonクラスに 依存している ● テストしたいコードがprivateになっている ● ゴッドクラス/モンスターメソッド ● Fat Activity/Fragment

Slide 34

Slide 34 text

コンストラクタのパラメータ化 34 ● クラス内部でインスタンスの生成をしている際に、コンストラク タのパタメータとして外から渡すようにする ● ユニットテストを書く際に不都合なDB・APIへのアクセスなどを テストダブルに差し替えることができる ● テストしたいコードから依存を切り離すための基本的なアプ ローチ

Slide 35

Slide 35 text

コンストラクタのパラメータ化 35 class ToDoItemRepository() {   val api = ToDoItemApi() fun save(toDoItem :ToDoItem) { api.save(toDoItem) } } @Test fun test() { val repository = ToDoItemRepository() save(ToDoItem("walking")) } APIへの送信が行われてしまう

Slide 36

Slide 36 text

コンストラクタのパラメータ化 36 class ToDoItemRepository(val api: ToDoItemApi) { fun save(toDoItem: ToDoItem) { api.save(toDoItem) } } @Test fun test() { val mockApi = mock(ToDoItemApi::class.java) val repository = ToDoItemRepository(mockApi) save(ToDoItem("walking")) verify(mockApi).save(ToDoItem("walking")) } モックオブジェクトをコンストラクタに渡すこ とでAPIの通信が行われない かつ与えた値をverifyで検証可能になる

Slide 37

Slide 37 text

静的setメソッドの導入 37 ● Singletoneクラスにインスタンスを差し替えるsetterを作成し、 テストダブルを差し込めるようにする ● テストしたいクラスがSingletonクラスを参照していて、テストを 書くのが難しい場合に有効 ● setterには@VisibleForTestingアノテーションをつけてあげる

Slide 38

Slide 38 text

静的setメソッドの導入 38 class ToDoItemRepository(val api: ToDoItemApi) { fun save(toDoItem: ToDoItem) { // singletonなクラスへのアクセス Analytics.log("save todo item") api.save(toDoItem) } } @Test fun test() { val repository = ToDoItemRepository(mockApi) save(ToDoItem("walking")) verify(mockApi).save(ToDoItem("walking")) } Analyticsへの送信が行われてしまう

Slide 39

Slide 39 text

静的setメソッドの導入 39 class Analytics private constructor() { companion object { private var instance: Analytics? = null fun getInstance(): Analytics { return instance } @VisibleForTesting fun setInstance(analytics: Analytics) { instance = analytics } } fun log(event: String) { } } Analyticsインスタンス差し替え setterを作成

Slide 40

Slide 40 text

静的setメソッドの導入 40 @Test fun test() { val mockAnalytics = mock(Analytics::class.java) Analytics.setInstance(mockAnalytics) val repository = ToDoItemRepository(mockApi) save(ToDoItem("walking")) verify(mockApi).save(ToDoItem("walking")) } モックオブジェクトをインスタンスに セットすることでAnalyticsへの 送信が行われないようにする

Slide 41

Slide 41 text

@VisibleForTesting ● 可視性をテスタビリティのために広く定義していることを示すア ノテーション ● アノテーションがつけられたクラス・メソッド・フィールドをプロダ クトコードから呼び出すと、InspectionでWarningがでる 41

Slide 42

Slide 42 text

privateメソッドのテスト 42 ● privateメソッドを新しいクラスとして抽出できないか? − クラスが多くのことを行いすぎていることが多い ● publicメソッドを通じてテストが可能か? ● 上記2つが難しい場合、メソッドに@VisibleForTestingアノテー ションをそえた上で、package privateやinternalにする Kotlinのアクセス修飾子 同じモジュール内からアクセス可の意

Slide 43

Slide 43 text

テストのためにsetter作成や可視性をさげること 43 ● 理想的な設計ではない − パラメータで渡せるようにしたり、クラスとして抽出するべき − 理想的な設計にするためには、大規模な改修になってしまうこともある ● テストを整備しておけば、あとでコードをもっときれいすること が可能 ● @VisibleForTestingを修正時の目印にする

Slide 44

Slide 44 text

スプラウトメソッド・スプラウトクラス 44 ● 要件を追加する際に新しいメソッド or クラスとして実装し、既 存のコードには新しいメソッド or クラスの呼び出しのみ追加す る ● 新しく追加したコードにはテストを作成する ● 既存のコードにテストを書くのは諦める ● ゴッドクラス・モンスターメソッド・FatActivty(Fragment)との戦 闘を一旦回避するために有効

Slide 45

Slide 45 text

スプラウトメソッド・スプラウトクラス 45 スプラウトクラス 依存が多くて インスタンスを 作るのが大変... すでにクラスが 大きくて 変更したくない...

Slide 46

Slide 46 text

リファクタリング 46 ● リファクタリングはどのように進めていく? ● 『リファクタリング』本の内容は現役で参考になる ● 本の内容をチャートでまとめてくれているサイトもあるので、リ ファクタリング時に対象のクラスにどの方針をとればいいか確 認してみる − http://objectclub.jp/technicaldoc/refactoring/refact-smell

Slide 47

Slide 47 text

IntelliJのリファクタリング機能 47 ● レガシーコードの改善&リファクタリング時に、IntelliJのリファク タリング機能を使うことでより安全に作業ができる − https://pleiades.io/help/idea/refactoring-source-code.html ● ショートカット例 − ⌃T : 使用できるリファクタリング機能の一覧表示 − option + ⌘ + M : メソッドとして切り出す(Extract Method) − option + ⌘ + P : パラメータとして切り出す(Extract Parameter) − クラスを選択するとコンストラクタのパラメータとして切り出される