Pro Yearly is on sale from $80 to $50! »

Androidテストハンズオン: テストのないアプリにテストを書こう編 / handson-write-tests-for-app-without-test

7867fe52a9be4257508a516d4df61578?s=47 tkmnzm
April 11, 2019

Androidテストハンズオン: テストのないアプリにテストを書こう編 / handson-write-tests-for-app-without-test

7867fe52a9be4257508a516d4df61578?s=128

tkmnzm

April 11, 2019
Tweet

Transcript

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

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

  3. アジェンダ • レガシーコードを改善しよう • レガシーコード改善に役立つ知識 − テストダブル − レガシーコード改善テクニック •

    テストのないアプリにテストを書こう 3
  4. レガシーコードを改善しよう 4

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

  6. テストが書きにくい設計になっている • 新規に実装する − TDD等の手法を駆使しつつ、テストが書きやすい設計にする • 既存のコードはどうする? − ごっそり書き直す VS

    地道に改善を重ねる − ごっそり書き直すチャンスを得られるのは稀。そして成功する保証はない。 6
  7. 地道に改善を重ねる • レガシーコード改善のすすめ − レガシーコードとは、単にテストのないコード(レガシーコード改善ガイドより) − テストを実装しながらテストが書きにくい設計を改善していく • 改善をしないとどうなる? −

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

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

    変更とリファクタリングを行う 9
  10. レガシーコード改善に役立つ知識 10

  11. 引用・参考について前置き • この章の内容・ソースコードは下記本の内容を引用・参考にし て作成しています。 − 『Androidテスト全書』 − 著者: 白山 文彦,

    外山 純生, 平田 敏之, 菊池 紘, 堀江 亮介 − https://peaks.cc/books/android_testing − 『レガシーコード改善ガイド』 − 著者: マイケル・C.フェザーズ 11
  12. テストダブル • テスト対象が依存しているコンポーネントを本物そっくりに振る 舞う代役(ダブル)と差し替えることで、自分の期待する挙動や 値の返却を達成する • レガシーコード改善の中では、テスト対象が依存しているクラ ス・コントロールしたいクラスをテストダブルに置き換えながら テストを整備していく 12

  13. テストダブル • 5つのパターン − スタブ − モック − スパイ −

    フェイク − ダミー 13
  14. スタブ • 事前に定義した任意の値をテスト対象に与える • 依存コンポーネントをスタブに置き換えることで、テスト対象に 任意の値を渡すことができる 14

  15. スタブ実装例(テスト対象コード) 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を返すメソッドをテストする
  16. 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 任意の値を返却する設定
  17. スタブの機能を実装する open class Satellite { open fun getWeather(): Weather {

    /* 元々の実装 */ } } /* 任意のWeatherを返すことができるスタブ */ class StubSatellite(val anyWeather: Weather) : Satellite() { override fun getWeather(): Weather { return anyWeather } } 17 任意の値を渡す 設定した任意の値を 返却
  18. モック 18 • テスト対象が依存コンポーネントに与える値や挙動(出力)を検 証する

  19. モック実装例(テスト対象コード) public class WeatherForecast { private WeatherRecorder recorder; void recordCurrentWeather(Weather

    weather) { recorder.record(weather); } } 19 内部でWeatherRecorder#recordを呼び出し ているメソッドをテストする
  20. Mockito(Java) WeatherRecorder mock = mock(WeatherRecorder.class); WeatherForecast testTarget = new WeatherForecast(mock);

    testTarget.recordCurrentWeather(Weather.SUNNY); verify(mock).record(Weather.SUNNY); Libraryを用いたモックの作成 20 メソッドが呼び出されたかと 与えられた値の検証
  21. モックの機能を実装する 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 } } メソッドの呼び出しと 与えられた値を記録
  22. スパイ 22 • テスト対象が依存コンポーネントに与える値や挙動(出力)を記 録する • モックライブラリで提供されているスパイは、 実際にメソッドコールを行う振る舞いをすることが多い

  23. スパイ実装例(テスト対象コード) public class WeatherForecast { private WeatherRecorder recorder; void recordCurrentWeather(Weather

    weather) { recorder.record(weather); } } 23 モックの際と同様のメソッドをテスト
  24. Libraryを用いたスパイの作成 Mockito(Java) WeatherRecorder spy = spy(new WeatherRecorder()); WeatherForecast testTarget =

    new WeatherForecast(mock); testTarget.recordCurrentWeather(Weather.SUNNY); verify(mock).record(Weather.SUNNY); 24 クラスのインスタンスを 渡して生成する
  25. スパイの機能を実装する 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) } } 実クラスのメソッドを 呼び出し
  26. [補足] 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
  27. フェイク・ダミー 27 • フェイク − 実際のコンポーネントと同等かそれに極めて近い挙動を持つ実装オブジェクト − 例: RoomのInMemoryDatabase •

    ダミー − テストの結果に影響を与えないが、テスト対象クラスの生成・メソッドの呼び出 しに使用する代替オブジェクト
  28. テストダブルの使いどころ 28 テストしたいクラス 依存しているクラス APIとの通信で実行時間がかかる など、いやな副作用がある 特定の振る舞いをテストしたいが、 再現するのが大変 依存しているクラスのメソッドが正し い引数で呼ばれているかわからな

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

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

    理を実行しない
  31. レガシーコード改善テクニック 31

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

  33. テストの書きにくいコードが抱える課題 33 • テストしたいコードがAPI通信などテストコードで 実行したくない機能に依存している • テストしたいコードがグローバルなSingletonクラスに 依存している • テストしたいコードがprivateになっている

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

    ローチ
  35. コンストラクタのパラメータ化 35 class ToDoItemRepository() {   val api = ToDoItemApi() fun

    save(toDoItem :ToDoItem) { api.save(toDoItem) } } @Test fun test() { val repository = ToDoItemRepository() save(ToDoItem("walking")) } APIへの送信が行われてしまう
  36. コンストラクタのパラメータ化 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で検証可能になる
  37. 静的setメソッドの導入 37 • Singletoneクラスにインスタンスを差し替えるsetterを作成し、 テストダブルを差し込めるようにする • テストしたいクラスがSingletonクラスを参照していて、テストを 書くのが難しい場合に有効 • setterには@VisibleForTestingアノテーションをつけてあげる

  38. 静的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への送信が行われてしまう
  39. 静的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を作成
  40. 静的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への 送信が行われないようにする
  41. @VisibleForTesting • 可視性をテスタビリティのために広く定義していることを示すア ノテーション • アノテーションがつけられたクラス・メソッド・フィールドをプロダ クトコードから呼び出すと、InspectionでWarningがでる 41

  42. privateメソッドのテスト 42 • privateメソッドを新しいクラスとして抽出できないか? − クラスが多くのことを行いすぎていることが多い • publicメソッドを通じてテストが可能か? • 上記2つが難しい場合、メソッドに@VisibleForTestingアノテー

    ションをそえた上で、package privateやinternalにする Kotlinのアクセス修飾子 同じモジュール内からアクセス可の意
  43. テストのためにsetter作成や可視性をさげること 43 • 理想的な設計ではない − パラメータで渡せるようにしたり、クラスとして抽出するべき − 理想的な設計にするためには、大規模な改修になってしまうこともある • テストを整備しておけば、あとでコードをもっときれいすること

    が可能 • @VisibleForTestingを修正時の目印にする
  44. スプラウトメソッド・スプラウトクラス 44 • 要件を追加する際に新しいメソッド or クラスとして実装し、既 存のコードには新しいメソッド or クラスの呼び出しのみ追加す る

    • 新しく追加したコードにはテストを作成する • 既存のコードにテストを書くのは諦める • ゴッドクラス・モンスターメソッド・FatActivty(Fragment)との戦 闘を一旦回避するために有効
  45. スプラウトメソッド・スプラウトクラス 45 スプラウトクラス 依存が多くて インスタンスを 作るのが大変... すでにクラスが 大きくて 変更したくない...

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

    − http://objectclub.jp/technicaldoc/refactoring/refact-smell
  47. IntelliJのリファクタリング機能 47 • レガシーコードの改善&リファクタリング時に、IntelliJのリファク タリング機能を使うことでより安全に作業ができる − https://pleiades.io/help/idea/refactoring-source-code.html • ショートカット例 −

    ⌃T : 使用できるリファクタリング機能の一覧表示 − option + ⌘ + M : メソッドとして切り出す(Extract Method) − option + ⌘ + P : パラメータとして切り出す(Extract Parameter) − クラスを選択するとコンストラクタのパラメータとして切り出される