Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Androidアプリの良いユニットテストを考える / Thinking about good unit tests for Android apps

tkmnzm
September 15, 2023

Androidアプリの良いユニットテストを考える / Thinking about good unit tests for Android apps

DroidKaigi 2023 セッション「Androidアプリの良いユニットテストを考える」の発表資料です。
https://2023.droidkaigi.jp/timetable/495066/

tkmnzm

September 15, 2023
Tweet

More Decks by tkmnzm

Other Decks in Technology

Transcript

  1. Androidアプリの
    良いユニットテストを考える
    Nozomi Takuma
    DroidKaigi 2023

    View Slide

  2. 自己紹介
    ● Nozomi Takuma
    ● DeNA SWETグループ
    ● Androidとテストが好き

    View Slide

  3. 今日話すことのゴール

    View Slide

  4. 今日話すことのゴール
    ● 良いユニットテストは何か?を考えることで、どのような
    自動テストを書くのが適しているか悩む場面に遭遇した際
    にメリット・デメリットを踏まえて方針決定ができるよう
    になる
    ● 既にある自動テストをより良いテストに改善できるように
    なる

    View Slide

  5. 目次
    ● 良いユニットテストとは何か
    ● Androidアプリの自動テスト環境
    ● テストダブルのトレードオフ
    ● 良いユニットテストになるように改善する

    View Slide

  6. 良いユニットテストとは何か

    View Slide

  7. なぜテストを自動化するのか
    ● アプリの変更が容易な状態を保つため
    ○ 自動化されたテストによるセーフティーネットがリグレッション
    から守ってくれる状態にすることでアプリの成長を支える
    ○ アプリが成長するに従って複雑化しても、リファクタリング等
    内部構造の改善を安心してできるようにする

    View Slide

  8. なぜユニットテストをするのか
    ● テストしたいすべてのパターンをE2Eテストでカバーするのは難しい
    ○ 安定性
    ○ フィードバックループの長さ
    ○ 検証したいパスに到達する難しさ
    ● アプリのコードを区切ってテストすることで、これらの問題を解決
    したい

    View Slide

  9. フィードバックループの長さ
    ● テストの実行時間
    ● 書いたコードを動作確認ができるまでの時間
    ● テストが失敗したときにどこに問題があるのかを見つけるまでの時間

    View Slide

  10. 検証したいパスに到達するための難しさ
    関数A
    関数B
    関数C
    関数D
    処理の分岐
    関数E
    関数F
    関数G
    関数H
    関数H

    View Slide

  11. 検証したいパスに到達するための難しさ
    関数A
    関数B
    関数C
    関数D
    関数E
    関数F
    関数G
    関数H
    関数H
    ここを検証したい
    処理の分岐

    View Slide

  12. 検証したいパスに到達するための難しさ
    関数A
    関数B
    関数C
    関数D
    関数E
    関数F
    関数G
    関数H
    関数H
    関数Eからテストが始められれば簡単
    処理の分岐

    View Slide

  13. 処理の分岐
    検証したいパスに到達するための難しさ
    関数A
    関数B
    関数C
    関数D
    関数E
    関数F
    関数G
    関数H
    関数H
    関数Aからテストを始める場合、
    その前の分岐も考慮する必要がある

    View Slide

  14. ユニットテストが目指すもの
    ● 結合範囲が広いテストが持つ課題をクリアする
    ○ 安定性
    ○ フィードバックループの長さ
    ○ 検証したいパスに到達する難しさ
    ● その上でソフトウェアの変更が容易な状態を保つのに貢献する

    View Slide

  15. ソフトウェアの変更が容易な状態にテストがどう貢献する?
    ● テストが開発者から信頼されている
    ● テストをグリーンに保つコストが低い
    ● テストを保守するコストが低い

    View Slide

  16. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  17. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードが保守しやすい
    結合範囲が広いテストが持つ課題から
    考えたもの

    View Slide

  18. 良いユニットテストとは何か
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードが保守しやすい
    ソフトウェアの変更が容易な状態を
    作るために必要なことから考えたもの

    View Slide

  19. 再現性と独立性がありテストが安定している
    ● 再現性を下げるものから切り離されている
    ● テストが他のテストや環境の影響を受けずに独立している

    View Slide

  20. 迅速なフィードバックを得られる
    ● テストの実行時間が短い
    ● 書いたコードを動作確認ができるまでの時間が短い
    ● テストが失敗したときにどこに問題があるのかを見つけるまでの
    時間が短い

    View Slide

  21. テストしたい箇所のテストを実行するのが容易
    ● テストを実行するためのセットアップが容易
    ● テストしたいパターンを網羅するのが容易

    View Slide

  22. リグレッションを検知できる
    ● 実装に問題があったときにテストが検知できる
    ○ 保護すべき箇所に自動テストがなく、問題のあるコードがリリース
    されてしまう状況が少ない
    ○ 保護すべき箇所に自動テストがあったにも関わらず、テストの
    書き方の問題でテストは成功している状況が少ない

    View Slide

  23. テストによる誤検知が少ない
    ● 実装には問題がないにも関わらず、テストだけが失敗してしまう頻度
    が少ない
    ○ リファクタリングによって振る舞いが変わらないにも関わらず
    テストが失敗する頻度が少ない
    ○ 新機能追加によって既存のテストが失敗する頻度が少ない

    View Slide

  24. テストコードが保守しやすい
    ● テストコードが読みやすく理解しやすい
    ● テストコードの修正・追加がしやすい

    View Slide

  25. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  26. 良いユニットテストの特徴を整理する上で参考にした書籍
    Vladimir Khorikov (著), 須田智之 (翻訳)
    「単体テストの考え方/使い方 」
    マイナビ出版, 2022

    View Slide

  27. 良いユニットテストの特徴を整理する上で参考にした書籍
    竹辺 靖昭 (監修), Titus Winters (編集), Tom Manshreck (編集),
    Hyrum Wright (編集), 久富木 隆一 (翻訳)
    「Googleのソフトウェアエンジニアリング
    ―持続可能なプログラミングを支える技術、文化、プロセス」
    オライリージャパン, 2021

    View Slide

  28. 良いユニットテストの特徴を整理する上で参考にした書籍
    Gerard Meszaros (著)
    「xUnit Test Patterns: Refactoring Test Code 」
    Addison-Wesley Professional, 2007

    View Slide

  29. Androidアプリの自動テスト環境

    View Slide

  30. Instrumentation Test
    (androidTest/src)
    実機
    Emulator
    Local Test
    (test/src)
    Robolectric
    RobolectricではないLocal Test
    自動テストの実行環境

    View Slide

  31. Robolectric
    ● JVM上でAndroidフレームワークをエミュレートしてくれるテスト
    フレームワーク
    ● Instrumentation TestでなくてもAndroidフレームワークに依存した
    テストを実装できる

    View Slide

  32. 自動テストの実行環境
    同じマシン上で実行
    Emulator 実機
    Instrumentation Test
    Robolectric
    Robolectric
    ではない
    Local Test
    Local Test

    View Slide

  33. 自動テストの実行環境
    Emulator 実機
    Robolectric
    Robolectric
    ではない
    Local Test
    Local Test
    Instrumentation Test
    今回は実機とEmulator
    の区分は重要ではない
    のでまとめて考える
    同じマシン上で実行

    View Slide

  34. 自動テストの実行環境
    Emulator 実機
    Robolectric
    Robolectric
    ではない
    Local Test
    Instrumentation Test
    同じマシン上で実行
    Local Testの2つは分けて考える
    以降、RobolectricではないLocal Testは
    Pure Local Testとする

    View Slide

  35. 実行環境ごとの特徴
    Instrumentation Test
    Robolectric
    Pure Local Test
    速い
    遅い
    実行速度
    実環境への
    近さ
    遠い
    近い

    View Slide

  36. 実環境への近さ
    ● Pure Local TestではAndroidフレームワークのコードを呼び出すこと
    はできないが、RobolectricとInstrumentation Testはできる
    ● Robolectricはフレームワークのコードが動かない箇所を独自の実装
    に置き換わるため、実環境への近さはInstrumentaion Testのほうが
    近い
    ● 以前UIテストはInstrumentaion Testでしかできなかったが、
    RobolectricでもInstrumentaion Testと同じテスト実装で同じテスト
    結果を得られるような実装が進められてきた

    View Slide

  37. 実環境への近さ
    Androidフレームワークに
    依存するコード
    ● View、Compose
    ● Activity、Fragment
    ● Context、Resources
    ● SQLite、SharedPreferences
    等を使ったコード
    Androidフレームワークに
    依存しないコード
    その他アプリのロジックや
    通信処理等

    View Slide

  38. 実行速度の違い
    ● 実行環境の特徴によりAndroidフレームワークに依存したコードの
    テストは、依存しないコードと比べるとテストの実行時間が伸びがち
    ● Robolectricはテストクラスの中で最初に実行されたテストにセット
    アップのオーバヘッドがかかり、それ以降は高速に実行される
    ● Instrumentation Testはテストケース1件1件が遅くなりやすい

    View Slide

  39. 実行速度の違い
    @Test
    fun test(){
    assertEquals(4, 2 + 2)
    }
    Local TestとRobolectricで
    同じテストを手元で実行する

    View Slide

  40. 実行速度の違い
    @Test
    fun test(){
    assertEquals(4, 2 + 2)
    }
    テストクラス全体の実行時間
    (テストメソッドは1件)
    Pure Local Test 2ms
    Robolectric 1sec200ms

    View Slide

  41. 実行速度の違い
    @get:Rule val composeRule = createComposeRule()
    @Test
    fun test() {
    composeRule.setContent {
    Greeting("hello")
    }
    composeRule.onNodeWithText("hello")
    }

    View Slide

  42. 実行速度の違い
    @get:Rule val composeRule = createComposeRule()
    @Test
    fun test() {
    composeRule.setContent {
    Greeting("hello")
    }
    composeRule.onNodeWithText("hello")
    }
    簡単なUIテストをRobolectricと
    Instrumentation Testで実行

    View Slide

  43. 実行速度の違い
    @get:Rule val composeRule = createComposeRule()
    @Test
    fun test() {
    composeRule.setContent {
    Greeting("hello")
    }
    composeRule.onNodeWithText("hello")
    }
    テストクラス全体の実行時間
    (テストメソッドは1件)
    Robolectric 1sec787ms
    Instrumentation Test 1sec

    View Slide

  44. 実行速度の違い
    @get:Rule val composeRule = createComposeRule()
    @Test
    fun test() {
    composeRule.setContent {
    Greeting("hello")
    }
    composeRule.onNodeWithText("hello")
    }
    テストクラスの中で同じテストを3回実行すると
    Robolectric
    1sec858ms
    (2回目以降は数十msで実行)
    Instrumentation Test 5sec

    View Slide

  45. 実行環境ごとの特徴
    Instrumentation Test
    Robolectric
    Pure Local Test
    速い
    遅い
    実行速度
    実環境への
    近さ
    遠い
    近い

    View Slide

  46. 実行環境ごとの特徴
    Instrumentation Test
    Robolectric
    Pure Local Test
    速い
    遅い
    実行速度
    実環境への
    近さ
    遠い
    近い
    CI環境では実行速度に加えて端末と接続するコストが
    追加され、他2つの環境との実行時間の差が更に広がる

    View Slide

  47. 自動テスト全体の実行速度を最大化する
    ● Androidフレームワークに依存したコードと依存していないアプリの
    ロジックを切り離してPure Local Testで実行できるようにする
    ● Androidフレームワークに依存したコードはRobolecticで実行する
    ● Robolectricでは忠実度が不足している箇所をInstrumentation Test
    で実行する

    View Slide

  48. 目次
    ● 良いユニットテストとは何か
    ● Androidアプリの自動テスト環境
    ● テストダブル(後述)のトレードオフ
    ● 良いユニットテストになるように改善する

    View Slide

  49. テストダブルのトレードオフ

    View Slide

  50. テストダブル
    ● テスト対象が依存しているオブジェクトの代役になるテスト用の実装
    ● Androidアプリ開発ではMoccKやMockitoといったライブラリが使われ
    ることが多い
    ● 今回の話はテストダブルのうち、スタブ・スパイ・フェイクがメイン

    View Slide

  51. テストダブル
    スタブ
    依存オブジェクトからテスト対象への入力
    を置き換える
    スパイ
    テスト対象から依存オブジェクトへの出力
    を検証できるようにする
    フェイク
    依存オブジェクトと同じように振る舞う
    テスト用の実装

    View Slide

  52. テストダブル
    スタブ
    依存オブジェクトからテスト対象への入力
    を置き換える
    スパイ
    テスト対象から依存オブジェクトへの出力
    を検証できるようにする
    フェイク
    依存オブジェクトと同じように振る舞う
    テスト用の実装
    モックライブラリでは
    1つのモックオブジェクトで両方の
    機能を使えるようになっている

    View Slide

  53. テストダブルのメリット
    ● テストの決定性をあげて常に同じ結果になるようにする
    ● テストの実行速度をあげる
    ● 再現の難しい状態を再現できる

    View Slide

  54. テストダブルのデメリット
    ● 実オブジェクトの振る舞いが変わったときに、テストダブルもそれを
    模倣するように修正する必要がある。修正が漏れて振る舞いがズレた
    ままだと、テストがリグレッションを検知できない可能性がある
    ● 依存オブジェクトへの出力の検証をやりすぎると、テスト対象の実装
    とテストコードが密接に結びついてしまい、少しの修正やリファクタ
    リングであってもテストの失敗につながる可能性がある

    View Slide

  55. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  56. 良いユニットテストとテストダブルの関係
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードが保守しやすい
    テストダブルはこの3つを実現するための手段
    の1つ

    View Slide

  57. 良いユニットテストとテストダブルの関係
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードが保守しやすい
    テストダブルがこの2つに悪影響を
    与える可能性がある

    View Slide

  58. テストダブルによるリグレッション検知失敗の例
    // 検索結果がないときはResult.FailureでItemNotFondExceptionを返す
    interface SearchRepository {
    suspend fun search(keyword: String): Result>
    }
    // リポジトリを使っているクラスのテストコード
    // 検索結果がない場合のテストでのスタブ(依存オブジェクトからの入力)設定
    val searchRepository = mockk {
    coEvery {
    search(any())
    } returns Result.failure(ItemNotFondException())
    }

    View Slide

  59. テストダブルによるリグレッション検知失敗の例
    // 検索結果がないときはResult.FailureでItemNotFondExceptionを返す
    interface SearchRepository {
    suspend fun search(keyword: String): Result>
    }
    // リポジトリを使っているクラスのテストコード
    // 検索結果がない場合のテストでのスタブ(依存オブジェクトからの入力)設定
    val searchRepository = mockk {
    coEvery {
    search(any())
    } returns Result.failure(ItemNotFondException())
    }
    検索結果が無いときは空のリストを返す
    ように振る舞いを変更しよう

    View Slide

  60. テストダブルによるリグレッション検知失敗の例
    // New: 検索結果がないときは空のアイテムを返すように変更する
    interface SearchRepository {
    suspend fun search(keyword: String): Result>
    }
    // リポジトリを使っているクラスのテストコード
    // 検索結果がない場合のテストでのスタブ(依存オブジェクトからの入力)設定
    val searchRepository = mockk {
    coEvery {
    search(any())
    } returns Result.failure(ItemNotFondException())
    }

    View Slide

  61. テストダブルによるリグレッション検知失敗の例
    // New: 検索結果がないときは空のアイテムを返すように変更する
    interface SearchRepository {
    suspend fun search(keyword: String): Result>
    }
    // リポジトリを使っているクラスのテストコード
    // 検索結果がない場合のテストでのスタブ(依存オブジェクトからの入力)設定
    val searchRepository = mockk {
    coEvery {
    search(any())
    } returns Result.failure(ItemNotFondException())
    }
    Repositoryに依存しているクラスではエラーの内容を
    見て検索結果がないときの実装をしていたところを
    修正する必要があるが、それを忘れていた...

    View Slide

  62. テストダブルによるリグレッション検知失敗の例
    // New: 検索結果がないときは空のアイテムを返すように変更する
    interface SearchRepository {
    suspend fun search(keyword: String): Result>
    }
    // リポジトリを使っているクラスのテストコード
    // 検索結果がない場合のテストでのスタブ(依存オブジェクトからの入力)設定
    val searchRepository = mockk {
    coEvery {
    search(any())
    } returns Result.failure(ItemNotFondException())
    }
    スタブは振る舞いをハードコーディングするため、
    スタブの設定の更新も必要

    View Slide

  63. テストダブルによるリグレッション検知失敗の例
    // New: 検索結果がないときは空のアイテムを返すように変更する
    interface SearchRepository {
    suspend fun search(keyword: String): Result>
    }
    // リポジトリを使っているクラスのテストコード
    // 検索結果がない場合のテストでのスタブ(依存オブジェクトからの入力)設定
    val searchRepository = mockk {
    coEvery {
    search(any())
    } returns Result.failure(ItemNotFondException())
    }
    スタブの更新も忘れてしまうと、検索結果がないときの
    挙動が間違っているにも関わらずテストは成功する

    View Slide

  64. 依存オブジェクトへの出力の検証でテストが失敗する例
    interface SearchRepository {
    fun search(keyword: String): Result>
    fun saveSearchKeywordHistory(keyword: String)
    }
    // リポジトリを使っているクラスの検索メソッド
    fun search(keyword: String) {
    searchRepository.saveSearchKeywordHistory(keyword)
    searchRepository.search(keyword)
    }

    View Slide

  65. 依存オブジェクトへの出力の検証でテストが失敗する例
    // リポジトリを使っているクラスのテスト
    // 依存オブジェクトへの出力(スパイ)を検証
    verify {
    searchRepository.saveSearchKeywordHistory(keyword)
    searchRepository.search(keyword)
    }

    View Slide

  66. 依存オブジェクトへの出力の検証でテストが失敗する例
    interface SearchRepository {
    fun search(keyword: String): Result>
    fun saveSearchKeywordHistory(keyword: String)
    }
    Repositoryのsearchメソッドの中で
    検索ワードの保存までやったほうがいいのでは?

    View Slide

  67. 依存オブジェクトへの出力の検証でテストが失敗する例
    interface SearchRepository {
    fun search(keyword: String): Result>
    }
    // リポジトリを使っているクラスの検索メソッド
    fun search(keyword: String) {
    searchRepository.search(keyword)
    }

    View Slide

  68. 依存オブジェクトへの出力の検証でテストが失敗する例
    // リポジトリを使っているクラスのテスト
    // 依存オブジェクトへの出力(スパイ)
    verify {
    searchRepository.saveSearchKeywordHistory(keyword)
    searchRepository.search(keyword)
    } テストは失敗する
    今回はコンパイルエラーでわかるパターン
    だが、実行時に失敗するケースもある

    View Slide

  69. テストダブルを使いすぎることの落とし穴
    ● テストダブルは実オブジェクトとのI/Fとテストで利用される振る舞
    いとの互換性を保つ必要があり、維持するコストがかかる
    ● テストダブルを使わなくてもいい場面では、実オブジェクトを使った
    ほうが互換性を気にしなくてもよくなる
    ● MockKやMockitoはfinalクラスであってもテストダブルが作れてしま
    うため、テストを書く簡単さを優先しすぎてテストダブルを多用しす
    ぎないように注意が必要

    View Slide

  70. スタブを使うべき箇所
    ● 現在時刻や乱数に依存する箇所
    ● APIや外部サービス等アプリの外部から入力を受け取る箇所
    ● BuildConfigなどappモジュールに実体があるものを、他モジュールで
    も参照している箇所
    ● 発生させるのが難しいエラーをエミュレートしたいとき
    ● 依存オブジェクトからの非同期処理に対して遅延している状態を
    エミュレートしたいとき

    View Slide

  71. スパイを使うべき箇所
    ● APIや外部サービスなどアプリの外部への出力をする箇所
    ● 依存オブジェクトのメソッドを呼び出す順序や呼び出す回数が
    重要な箇所

    View Slide

  72. テストダブルを使うか使わないかを検討する箇所
    ● SQLite・SharedPreferences・DataStore等端末内への永続化
    ● Androidフレームワークに依存している箇所
    ● アプリのコード内の設計上の境界

    View Slide

  73. 端末内への永続化
    ● 永続化の処理はInterface化されていることが多いが、必ずしもテス
    トダブルに差し替える必要はない
    ● Robolectricを使えばLocal TestでSQLiteやPreferenceが使用できる
    ○ DataStoreはRobolectricも不要

    View Slide

  74. 端末内への永続化
    ● 例えばRepositoryのテストでは、Robolectricを使ってSQLiteや
    Preferencesと結合させたテストにすることができる
    ○ 発生させるのが難しいエラーのテストをしたいときのみ
    テストダブルを使うこともできる
    ● 一方でRepositoryに依存している実装のテストでは、テストしたい
    ロジックに集中するためにRepositoryをテストダブルに置き換える
    こともできる

    View Slide

  75. 端末内への永続化
    ● 例えばRepositoryのテストでは、Robolectricを使ってSQLiteや
    Preferencesと結合させたテストにすることができる
    ○ 発生させるのが難しいエラーのテストをしたいときのみ
    テストダブルを使うこともできる
    ● 一方でRepositoryに依存している実装のテストでは、テストしたい
    ロジックに集中するためにRepositoryをテストダブルに置き換える
    こともできる
    テストダブルを使ったときと比べると、
    テストの実行速度は伸びるがテストダブルの互換性
    を維持するコストは減る

    View Slide

  76. 端末内への永続化
    ● 例えばRepositoryのテストでは、Robolectricを使ってDaoや
    Preferencesと結合させたテストにすることができる
    ○ 発生させるのが難しいエラーのテストをしたいときのみ
    テストダブルを使うこともできる
    ● 一方でRepositoryに依存している実装のテストでは、テストしたい
    ロジックに集中するためにRepositoryをテストダブルに置き換える
    こともできる
    実オブジェクトを使ったときと比べると、テストを書き始める
    コストが下がる&実行時間が短くなる
    一方でテストダブルの振る舞いの互換性を維持するコストが増える

    View Slide

  77. Androidフレームワークに依存している箇所
    ● まずはテストしたい箇所がRobolectricもテストダブルも使わずに
    テストできないかを検討する
    ○ 例: Serviceクラス内に実装されたロジックをServiceから切り離
    してテストできるようにする
    ● Robolectricを使えばAndroidフレームワークに依存している実装の
    Local Testも書けるが、テスト全体の実行速度を最大化するためには
    Robolectricを使う箇所も部分的であるほうが良い

    View Slide

  78. フェイク
    ● 依存オブジェクトと同じように振る舞うテスト用の実装
    ● 例えばAPI通信をするデータソースをオンメモリ上のデータを返す
    フェイクオブジェクトに置き換えることができれば、テストの実行
    速度を下げずにテストダブルの設定を減らすことができる
    ● スタブ・スパイを比較するとテストがリファクタリングに強くなる
    ● 必ずしも自分が欲しいフェイクが既に用意されているとは限らない

    View Slide

  79. フェイク
    interface UserDataSource {
    suspend fun getUser(): User
    suspend fun saveUser(user: User)
    }
    // テスト対象メソッド Userを保存し、最新のUserを取得する関数
    suspend fun updateUser(user: User) : User {
    userDataSource.saveUser(user)
    return userDataSource.getUser()
    }

    View Slide

  80. フェイク
    // スタブ・スパイを使ったテストのイメージ
    val user = User.filledUser()
    val userDataSource = mockk {
    coEvery { getUser() } returns user
    }
    val updatedUser = sut.updateUser(user)
    coVerify {
    userDataSource.saveUser(user)
    }
    assertEquals(User.filledUser(), updatedUser)

    View Slide

  81. フェイク
    // UserDataSourceのFake実装
    class FakeUserDataSource : UserDataSource {
    private var user: User = User.emptyUser()
    override suspend fun getUser(): User {
    return user
    }
    override suspend fun saveUser(user: User) {
    this.user = user
    }
    }

    View Slide

  82. フェイク
    // Fakeを使ったテストのイメージ
    val userDataSource = FakeUserDataStore()
    val updatedUser = sut.updateUser(User.filledUser())
    assertEquals(User.filledUser(), updatedUser)

    View Slide

  83. フェイク
    // Fakeを使ったテストのイメージ
    val userDataSource = FakeUserDataStore()
    val updatedUser = sut.updateUser(User.filledUser())
    assertEquals(User.filledUser(), updatedUser)
    テスト対象の公開されたAPIを使ったテストに
    なっているため、メソッドの中の変更に強い

    View Slide

  84. フェイク
    // スタブ・スパイを使ったテストのイメージ
    val user = User.filledUser()
    val userDataSource = mockk {
    coEvery { getUser() } returns user
    }
    val updatedUser = sut.updateUser(user)
    coVerify {
    userDataSource.saveUser(user)
    }
    assertEquals(user, updatedUser)
    スタブとスパイを使ったテストコードは
    テスト対象の実装の内部を見ている状態

    View Slide

  85. フェイクを実装する場合の注意点
    ● 作成のコストがかかる
    ● 実オブジェクトと振る舞いの互換性を維持するコストは必要
    ● 実オブジェクトと同じように振る舞うことをテストで確認するのが
    望ましい
    ● テストしたい条件によってはフェイクでは再現が難しく、スタブや
    スパイと組み合わせる必要がある

    View Slide

  86. フェイクを実装する場合の注意点
    ● フェイクの実装・維持コストが割に合うか
    ○ 複数のテスト対象から使われる箇所
    ○ 振る舞いの変更が少ない箇所
    ○ 複雑なロジックがない箇所

    View Slide

  87. テストダブルのトレードオフ
    ● 依存オブジェクトをテストダブルに置き換えることで、良いユニット
    テストの特徴のいくつかを実現できる
    ● テストダブルのメリット・デメリットを理解した上で、どこを置き換
    えるべきかを判断する必要がある
    ● テストダブルの使い過ぎはリグレッションの検知が失敗したり、
    テストが失敗しやすくなる原因につながる

    View Slide

  88. 良いユニットテストになるように改善する

    View Slide

  89. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  90. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  91. 再現性を改善する
    ● 再現性を下げる主なパターン
    ○ APIや外部サービスとのネットワーク通信
    ○ 時刻やランダム値の生成
    ○ 非同期処理

    View Slide

  92. 再現性を改善する
    ● 再現性を下げる主なパターン
    ○ APIや外部サービスとのネットワーク通信
    ○ 時刻やランダム値の生成
    ○ 非同期処理
    テストダブルを使って解決する

    View Slide

  93. 再現性を改善する
    ● 再現性を下げる主なパターン
    ○ APIや外部サービスとのネットワーク通信
    ○ 時刻やランダム値の生成
    ○ 非同期処理
    非同期処理のコントロール方法を知る

    View Slide

  94. 再現性を改善する
    Androidアプリのアーキテクチャにそってテストの書き方を学ぼう
    github.com/DeNA/android-modern-architecture-test-handson
    ● データレイヤをテストする
    ○ API通信をするコードのテストを実装しながらCoroutineの
    テストについて学ぶ
    ○ オンメモリキャッシュのテストを書く

    View Slide

  95. 独立性を改善する
    ● テストの独立性が下がる主なパターン
    ○ 永続化された情報
    ○ mutableなStatic変数
    ○ 実行環境

    View Slide

  96. 独立性を改善する
    ● テストの独立性が下がる主なパターン
    ○ 永続化された情報
    ○ mutableなStatic変数
    ○ 実行環境
    テストダブルを使うことでも解決できるが、
    RobolectricではSQLiteやPreferencesに保存
    したデータやContext配下に保存したファイル
    はテスト間で共有されず独立した状態
    (※Instrumentaion Testでは共有される)

    View Slide

  97. ● テストの独立性が下がる主なパターン
    ○ 永続化された情報
    ○ mutableなStatic変数
    ○ 実行環境
    独立性を改善する

    View Slide

  98. ● テストの独立性が下がる主なパターン
    ○ 永続化された情報
    ○ mutableなStatic変数
    ○ 実行環境
    独立性を改善する
    Static変数はどのテスト実行環境でも
    テスト間をまたいで共有される
    設計を見直すか、テスト終了時にクリー
    ンアップする手段を用意する

    View Slide

  99. ● テストの独立性が下がる主なパターン
    ○ 永続化された情報
    ○ mutableなStatic変数
    ○ 実行環境
    独立性を改善する
    よくあるのはタイムゾーンやロケール
    開発環境とCIのマシンで異なることが多い

    View Slide

  100. 独立性を改善する
    // 端末のロケールに合わせて値段の表記を決める関数
    fun formatPrice(price: BigDecimal): String {
    val numberFormat =
    NumberFormat.getCurrencyInstance(Locale.getDefault())

    }
    mutableなstatic変数かつ実行環境の影響を
    受ける状態

    View Slide

  101. 独立性を改善する
    // 端末のロケールに合わせて値段の表記を決める関数
    fun formatPrice(price: BigDecimal,
    locale: Locale = ..): String {
    val numberFormat =
    NumberFormat.getCurrencyInstance(locale)

    }
    Localeを引数に渡せるようにすることで、
    テストではLocaleを指定すれば他のテストや実行環境
    の影響を受けずに同じ結果を返すようになる

    View Slide

  102. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  103. 迅速なフィードバックを阻むもの
    ● APIや外部サービスとのネットワーク通信等、実行速度に大きな影響
    を与えるもの
    ● テストしにくい箇所と密結合になっている設計

    View Slide

  104. 迅速なフィードバックを阻むもの
    ● APIや外部サービスとのネットワーク通信等、実行速度に大きな影響
    を与えるもの
    ● テストしにくい箇所と密結合になっている設計
    極端な例だが、全ての実装がUIやコントローラにある場合、
    それらを通してでしかテストが書けなくなる
    テストしたい箇所をすぐにテストできるよう切り分けるのが
    基本的なアプローチになる

    View Slide

  105. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  106. テストしたい箇所のテストを実行するのが容易
    ● テストを実行するためのセットアップが容易
    ● テストしたいパターンを網羅するのが容易

    View Slide

  107. テストしたい箇所のテストを実行するのが容易
    ● テストを実行するためのセットアップが容易
    ● テストしたいパターンを網羅するのが容易
    テストダブルに差し替える以外の手段を紹介する

    View Slide

  108. テスト実行を容易にする工夫
    ● テストしにくい箇所をHumble Objectにし、テストしたい箇所を
    切り出す
    ● 生成をまとめる
    ○ テストデータ
    ○ テスト対象や依存オブジェクト

    View Slide

  109. Humble Objectパターン
    ● テストを難しくする依存を持つオブジェクトからロジックをできるだ
    け切り離してロジックをほとんど持たないクラスにする(Humble = つ
    つましい、質素)
    ● ロジックはテストを難しくする依存から切り離されて、テストがしや
    すくなる
    ● http://xunitpatterns.com/Humble Object.html

    View Slide

  110. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    if (value.length > 20) return
    _uiState.update {
    it.copy(
    user = it.user.copy(name = name),
    isSaveEnabled = value.isNotEmpty()
    )
    }
    }
    }

    View Slide

  111. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    if (value.length > 20) return
    _uiState.update {
    it.copy(
    user = it.user.copy(name = name),
    isSaveEnabled = value.isNotEmpty()
    )
    }
    }
    }
    このメソッドをつつましくする

    View Slide

  112. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    if (name.length > 20) return
    _uiState.update {
    it.copy(
    user = it.user.copy(name = name),
    isSaveEnabled = name.isNotEmpty()
    )
    }
    }
    }

    View Slide

  113. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    if (name.length > 20) return
    _uiState.update {
    it.copy(
    user = it.user.copy(name = name),
    isSaveEnabled = name.isNotEmpty()
    )
    }
    }
    }
    Userクラスで名前が20文字を超えて
    更新できないように制御しちゃう

    View Slide

  114. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    if (name.length > 20) return
    _uiState.update {
    it.copy(
    user = it.user.copy(name = name),
    isSaveEnabled = name.isNotEmpty()
    )
    }
    }
    }
    Userの情報を見てUI Stateのほうで
    判断してもらおう

    View Slide

  115. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    _uiState.update {
    it.copy(
    user = it.user.updateName(name = name)
    )
    }
    }
    }

    View Slide

  116. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    _uiState.update {
    it.copy(
    user = it.user.updateName(name = name)
    )
    }
    }
    }
    ViewModelの中からUIのStateを作るためのロジックや
    ドメインロジックが切り離された

    View Slide

  117. Humble Objectパターン
    class UserViewModel(..) {
    fun onNameChanged(name: String) {
    _uiState.update {
    it.copy(
    user = it.user.updateName(name = name)
    )
    }
    }
    }
    ViewModel経由ではなく、UserとUI Stateの単体テストで
    確認できる実装が増えた

    View Slide

  118. 生成をまとめる
    ● Creation Methodパターン
    ○ テスト対象や依存オブジェクトのインスタンス生成の複雑さを
    カプセル化する
    ○ http://xunitpatterns.com/Creation Method.html

    View Slide

  119. テストデータの生成をまとめる
    data class User(
    val lastName: String,
    val firstName: String,
    val birthDate: LocalDate?,
    val height: Float,
    val weight: Float,
    )

    View Slide

  120. テストデータの生成をまとめる
    // 必要なフィールドが埋まっているUserクラスをテスト間で共有する
    val emptyUser = User(
    lastName = "",
    firstName = "",
    birthDate = null,
    height = 0f,
    weight = 0f
    )

    View Slide

  121. テストデータの生成をまとめる
    // 保存ボタンが活性化する条件をテストしたい
    data class UiState(
    val user: User
    ) {
    val isSaveButtonEnable = user.lastName.isNotEmpty()
    && user.firstName.isNotEmpty()
    }

    View Slide

  122. テストデータの生成をまとめる
    val sut = UiState(
    user = User( // 各テストで直接Userのインスタンスを作っている場合
    lastName = "lastName",
    firstName = "firstName",
    birthDate = null, height = 0f, weight = 0f
    )
    )
    assertTrue(uiState.isSaveButtonEnable)

    View Slide

  123. テストデータの生成をまとめる
    val sut = UiState(
    user = User( // 各テストで直接Userのインスタンスを作っている場合
    lastName = "lastName",
    firstName = "firstName",
    birthDate = null, height = 0f, weight = 0f
    )
    )
    assertTrue(uiState.isSaveButtonEnable)
    Userにフィールドが追加されたら、Userを生成し
    ている全てのテストがコンパイルエラーになる

    View Slide

  124. テストデータの生成をまとめる
    val sut = UiState(
    user = emptyUser.copy( // 共通のユーザー定義を使う場合
    lastName = "lastName",
    firstName = "firstName",
    )
    )
    assertTrue(uiState.isSaveButtonEnable)

    View Slide

  125. テストデータの生成をまとめる
    val sut = UiState(
    user = emptyUser.copy( // 共通のユーザー定義を使う場合
    lastName = "lastName",
    firstName = "firstName",
    )
    )
    assertTrue(uiState.isSaveButtonEnable)
    テストに関係のあるフィールドだけセットする

    View Slide

  126. テストデータの生成をまとめる
    val sut = UiState(
    user = emptyUser.copy( // 共通のユーザー定義を使う場合
    lastName = "lastName",
    firstName = "firstName",
    )
    )
    assertTrue(uiState.isSaveButtonEnable)
    Userにフィールドが増えても修正するのは共通の定義だけで良い

    View Slide

  127. テスト対象や依存オブジェクトの生成をまとめる
    // テスト対象クラス
    class UserViewModel(
    val profileRepository: UserRepository,
    val userConfigRepository : UserConfigRepository
    )

    View Slide

  128. テスト対象や依存オブジェクトの生成をまとめる
    // テスト対象クラスの生成をまとめたテスト用ヘルパー
    private fun createViewModel(
    userRepository: UserRepository = TestUserRepository(),
    userConfigRepository : UserConfigRepository = ..
    ) : UserViewModel {
    return UserViewModel(
    profileRepository = profileRepository,
    userConfigRepository = userConfigRepository
    )
    }

    View Slide

  129. テスト対象や依存オブジェクトの生成をまとめる
    // テスト対象クラス
    class UserViewModel(
    val profileRepository: UserRepository,
    val userConfigRepository : UserConfigRepository,
    val logTracker : LogTracker, // New
    )

    View Slide

  130. テスト対象や依存オブジェクトの生成をまとめる
    // テスト対象クラス
    class UserViewModel(
    val profileRepository: UserRepository,
    val userConfigRepository : UserConfigRepository,
    val logTracker : LogTracker, // New
    )
    テストケース毎にコンストラクタの指定をして
    いるとそれぞれ修正する必要があるが、生成を
    1つにまとめていれば1箇所修正すればよい

    View Slide

  131. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● テスト対象をリグレッションから保護している
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  132. リグレッションを検知できる
    ● 実装に問題があったときにテストが検知できる
    ○ 保護すべき箇所に自動テストがなく、問題のあるコードがリリー
    スされてしまう状況が少ない
    ○ 保護すべき箇所に自動テストがあったにも関わらず、テストの書
    き方の問題でテストは成功している状況が少ない

    View Slide

  133. リグレッション検知失敗の例
    ● テストダブルと実オブジェクトの振る舞いの互換性がなく、失敗する
    べきテストが成功してしまう
    ● @Testをつけ忘れたり、Assertがなく検証ができていない
    ● Assertionするべき項目が間違っている
    ● 境界値チェックができておらず、境界値に実装ミスがあった
    ● 非同期処理でテストでは1つのスレッドで確認していたが、複数ス
    レッドで呼び出すと問題になる実装があった

    View Slide

  134. ● テストダブルと実オブジェクトの振る舞いの互換性がなく、失敗する
    べきテストが成功してしまう
    ● @Testをつけ忘れたり、Assertがなく検証ができていない
    ● Assertionするべき項目が間違っている
    ● 境界値チェックができておらず、境界値に実装ミスがあった
    ● 非同期処理でテストでは1つのスレッドで確認していたが、複数ス
    レッドで呼び出すと問題になる実装があった
    リグレッション検知失敗の例
    残念ながら仕組みで防ぐのが難しいものが多い
    テスト実装時やレビュー時にチェックする必要がある

    View Slide

  135. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● テスト対象をリグレッションから保護している
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  136. 実装には問題がないがテストが失敗する例
    ● テストコードがテスト対象の実装に密接に結びついていたために、
    リファクタリング後にテストが失敗する
    ● テストにロジックが入っていて、そのロジックが間違っている
    ● テストダブルやテストデータの設定ミス
    ● 非同期処理の設定ミス
    ● テスト対象への引数の追加によるコンパイルエラー

    View Slide

  137. 実装には問題がないがテストが失敗する例
    ● テストコードがテスト対象の実装に密接に結びついていたために、
    リファクタリング後にテストが失敗する
    ● テストにロジックが入っていて、そのロジックが間違っている
    ● テストダブルやテストデータの設定ミス
    ● 非同期処理の設定ミス
    ● テスト対象への引数の追加によるコンパイルエラー
    できるだけテストコードをシンプルにする

    View Slide

  138. 実装には問題がないがテストが失敗する例
    ● テストコードがテスト対象の実装に密接に結びついていたために、
    リファクタリング後にテストが失敗する
    ● テストにロジックが入っていて、そのロジックが間違っている
    ● テストダブルやテストデータの設定ミス
    ● 非同期処理の設定ミス
    ● テスト対象への引数の追加によるコンパイルエラー
    無くすのは難しいが、よく直面するケース
    生成をまとめることで発生箇所を限定するような
    工夫はできる

    View Slide

  139. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● テスト対象をリグレッションから保護している
    ● リグレッションを検知できる
    ● テストコードを保守しやすい

    View Slide

  140. 保守しやすいテストコード
    ● プロダクトコードとテストコードの比率は、テストコードのほうが
    高くなることもある
    ● プロダクトコードと同じようにテストコードも保守しやすい状態を
    保つ必要がある

    View Slide

  141. テストコードの保守性をあげる工夫
    ● 何をテストしているか理解しやすくする
    ● パラメタライズドテストを読みやすくする

    View Slide

  142. 何をテストしているか理解しやすくする
    @Test
    fun getUser_error() {
    val userRepository = TestUserRepository()
    val useViewModel = UserViewModel(userRepository)
    useViewModel.getUser()
    assertTrue(
    useViewModel.uiState.value.shouldDisplayRetryButton)
    }

    View Slide

  143. 何をテストしているか理解しやすくする
    @Test
    fun getUser_error() {
    val userRepository = TestUserRepository()
    val useViewModel = UserViewModel(userRepository)
    useViewModel.getUser()
    assertTrue(
    useViewModel.uiState.value.shouldDisplayRetryButton)
    }
    テストメソッド内を
    Arrange(準備)/Act(実
    行)/Assert(確認)のフェー
    ズで区切ってみよう

    View Slide

  144. 何をテストしているか理解しやすくする
    @Test
    fun getUser_error() {
    val userRepository = TestUserRepository()
    val useViewModel = UserViewModel(userRepository)
    useViewModel.getUser()
    assertTrue(
    useViewModel.uiState.value.shouldDisplayRetryButton)
    }

    View Slide

  145. 何をテストしているか理解しやすくする
    @Test
    fun getUser_error() {
    val userRepository = TestUserRepository()
    val useViewModel = UserViewModel(userRepository)
    useViewModel.getUser()
    assertTrue(
    useViewModel.uiState.value.shouldDisplayRetryButton)
    }
    テスト対象のコードが
    どのような振る舞いになるのか
    もっと明確にしてみよう

    View Slide

  146. 何をテストしているか理解しやすくする
    @Test
    fun`ユーザーの取得に失敗したときはリトライボタンを表示させる`() {
    val userRepository = TestUserRepository()
    val useViewModel = UserViewModel(userRepository)
    useViewModel.getUser()
    assertTrue(
    useViewModel.uiState.value.shouldDisplayRetryButton)
    }

    View Slide

  147. パラメタライズドテストを読みやすくする
    fun checkAge(age: Int) : Boolean {
    if(age in 18..59) {
    return true
    } else {
    logger.trackInvalidUser(OUT_OF_AGE_RANGE)
    return false
    }
    }

    View Slide

  148. パラメタライズドテストを読みやすくする
    fun checkAge(age: Int) : Boolean {
    if(age in 18..59) {
    return true
    } else {
    logger.trackInvalidUser(OUT_OF_AGE_RANGE)
    return false
    }
    }
    機能を利用可能な年齢かを認確する
    メソッド
    年齢が無効なときはログ送信

    View Slide

  149. パラメタライズドテストを読みやすくする
    TestCase(
    age : Int
    result : Boolean
    invalidUserReason: Int?,
    isLogTracked: Boolean
    )

    View Slide

  150. パラメタライズドテストを読みやすくする
    listOf(
    TestCase(17, false, OUT_OF_AGE_RANGE, true),
    TestCase(18, true, null, false),
    TestCase(59, true, null, false),
    TestCase(60, false, OUT_OF_AGE_RANGE, true),
    )

    View Slide

  151. パラメタライズドテストを読みやすくする
    listOf(
    TestCase(17, false, OUT_OF_AGE_RANGE, true),
    TestCase(18, true, null, false),
    TestCase(59, true, null, false),
    TestCase(60, false, OUT_OF_AGE_RANGE, true),
    )
    テストでメソッドで
    どう使っているか?

    View Slide

  152. パラメタライズドテストを読みやすくする
    @Test
    fun test() {
    ..
    if(testCase.isLogTracked) {
    assertEquals(testCase.invalidUserReason, actual)
    }
    }

    View Slide

  153. パラメタライズドテストを読みやすくする
    @Test
    fun test() {
    ..
    if(testCase.isLogTracked) {
    assertEquals(testCase.invalidUserReason, actual)
    }
    }
    1つのパラメタライズド
    テストで頑張りすぎてるかも

    View Slide

  154. パラメタライズドテストを読みやすくする
    listOf(
    TestCase(18),
    TestCase(59),
    )
    listOf(
    TestCase(17),
    TestCase(60),
    )
    テストをややこしくしていた
    パラメータをなくして、
    有効なケースと無効なケースに
    分けちゃう

    View Slide

  155. パラメタライズドテストを読みやすくする
    listOf(
    TestCase(18),
    TestCase(59),
    )
    なぜこのパラメータで
    テストしているんだろう?

    View Slide

  156. パラメタライズドテストを読みやすくする
    TestCase(
    caseName: String
    age : Int
    )
    テストケースの意図を
    説明できるパラメータを追加

    View Slide

  157. パラメタライズドテストを読みやすくする
    listOf(
    TestCase("利用可能年齢の範囲内の場合は有効とする(最小値)", 18),
    TestCase("利用可能年齢の範囲内の場合は有効とする(最大値)", 59),
    )

    View Slide

  158. まとめ

    View Slide

  159. ユニットテストが目指すもの
    ● 結合範囲が広いテストが持つ課題をクリアする
    ○ 安定性
    ○ フィードバックループの長さ
    ○ 検証したいパスに到達する難しさ
    ● その上でアプリの変更が容易な状態を保つのに貢献する

    View Slide

  160. 良いユニットテストの特徴
    ● 再現性と独立性がありテストが安定している
    ● 迅速なフィードバックを得られる
    ● テストしたい箇所のテストを実行するのが容易
    ● リグレッションを検知できる
    ● テストによる誤検知が少ない
    ● テストコードを保守しやすい

    View Slide

  161. 参考書籍

    View Slide

  162. 参考書籍
    Vladimir Khorikov (著), 須田智之 (翻訳)
    「単体テストの考え方/使い方 」
    マイナビ出版, 2022

    View Slide

  163. 参考書籍
    竹辺 靖昭 (監修), Titus Winters (編集), Tom Manshreck (編集),
    Hyrum Wright (編集), 久富木 隆一 (翻訳)
    「Googleのソフトウェアエンジニアリング
    ―持続可能なプログラミングを支える技術、文化、プロセス」
    オライリージャパン, 2021

    View Slide

  164. 参考書籍
    Gerard Meszaros (著)
    「xUnit Test Patterns: Refactoring Test Code 」
    Addison-Wesley Professional, 2007

    View Slide

  165. ご清聴ありがとうございました!

    View Slide