$30 off During Our Annual Pro Sale. View Details »

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

tkmnzm
April 11, 2019

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

tkmnzm

April 11, 2019
Tweet

More Decks by tkmnzm

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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を返すメソッドをテストする

    View Slide

  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
    任意の値を返却する設定

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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
    }
    }
    メソッドの呼び出しと
    与えられた値を記録

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)
    }
    }
    実クラスのメソッドを
    呼び出し

    View Slide

  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

    View Slide

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

    View Slide

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

    参照

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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で検証可能になる

    View Slide

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

    View Slide

  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への送信が行われてしまう

    View Slide

  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を作成

    View Slide

  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への
    送信が行われないようにする

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  44. スプラウトメソッド・スプラウトクラス
    44
    ● 要件を追加する際に新しいメソッド or クラスとして実装し、既
    存のコードには新しいメソッド or クラスの呼び出しのみ追加す

    ● 新しく追加したコードにはテストを作成する
    ● 既存のコードにテストを書くのは諦める
    ● ゴッドクラス・モンスターメソッド・FatActivty(Fragment)との戦
    闘を一旦回避するために有効

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide