|
| 1 | +--- |
| 2 | +slug: test-test-test |
| 3 | +title: Koject v1.3.0 - DIコンテナを使ったテストコードを書こう |
| 4 | +authors: atsushi |
| 5 | +image: /blog/2023-03-25/ogp.png |
| 6 | +--- |
| 7 | + |
| 8 | +import { |
| 9 | + KojectStart, |
| 10 | + KojectStartTest, |
| 11 | + KojectRunTest, |
| 12 | + Inject, |
| 13 | + TestProvides, |
| 14 | +} from '@site/src/components/CodeLink'; |
| 15 | + |
| 16 | +# Koject v1.3.0 - DIコンテナを使ったテストコードを書こう |
| 17 | + |
| 18 | + |
| 19 | + |
| 20 | +継続的なソフトウェア開発を行う上で、テストコードは重要な役割を果たします。 |
| 21 | +Koject v1.3.0ではテストのサポートが追加されました。 |
| 22 | +この記事ではテスト時にDIコンテナを使う理由と、Kojectを使ったテストコードの書き方、Koject v1.3.0で追加されたもう一つの機能について紹介します。 |
| 23 | + |
| 24 | +<!--truncate--> |
| 25 | + |
| 26 | +[**Read in English →**](/blog/test-test-test) |
| 27 | + |
| 28 | +## テスト時もDIコンテナを使う |
| 29 | +[以前紹介](/blog/jp/first-stable-release)したように、Dependency Injectionのパターンに従うことで、テスト容易性を向上させることができます。 |
| 30 | +テスト時に一部の依存関係を差し替えることで、外部との通信をなくして不安定なテストを避けたり、テストにかかる時間を短縮することが可能になります。 |
| 31 | + |
| 32 | +この際に利用される、実際とは異なるオブジェクトのことを、モックやフェイクと言います。 |
| 33 | +一方で、単一のクラスをテストするために、そのクラスの全ての依存関係を実際とは異なるモックに差し替えると、テストの信頼性や保守性の面で問題が生じるかもしれません。 |
| 34 | + |
| 35 | +```kotlin |
| 36 | +class VideoUploadServiceTest { |
| 37 | + private val videoUploader = mock(VideoUploader::class) |
| 38 | + private val notificationManager = mock(NotificationManager::class) |
| 39 | + private val videoUploadService = |
| 40 | + VideoUploadService(videoUploader, notificationManager) |
| 41 | + |
| 42 | + @Test |
| 43 | + fun test() { |
| 44 | + /* ... */ |
| 45 | + } |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +モックオブジェクトは実際のオブジェクトと全く同じ動作をするわけではないので、それが理由でテストが失敗する可能性があります。 |
| 50 | +テストが失敗した際に実際のコードが間違っているのか、もしくはうまくモックできていないのか、判断するのに手間がかかるようになります。 |
| 51 | +反対に、モックが正しくないことにより、コードが間違っているのにテストが成功する可能性も存在します。 |
| 52 | + |
| 53 | +テストの対象のほとんどがモックの場合、あまり意味のあるテストとは言えないでしょう。 |
| 54 | + |
| 55 | +また、モックオブジェクトが増えるとその保守も難しくなります。 |
| 56 | +モックしている対象が変わった場合、そのモックオブジェクトも追従する必要があります。 |
| 57 | +メンテしにくいテストは次第に機能しなくなるため、テストのメンテナンス性は重要です。 |
| 58 | + |
| 59 | +できる限り実際の依存関係を利用することをおすすめします。 |
| 60 | +制御が難しい外部と通信する場合や、そのままだと莫大な時間がかかる場合に、その該当箇所のみをテスト用のものに差し替えます。 |
| 61 | + |
| 62 | +テスト対象のクラスが多くの依存関係を持つ場合、インスタンスの生成に苦労すると考えるかもしれません。 |
| 63 | +心配しないでください。 |
| 64 | +本番環境と同じくDIコンテナを使えば、テスト時の依存関係も簡単に作成することができます。 |
| 65 | + |
| 66 | +## UnitテストでKojectを使う |
| 67 | + |
| 68 | +テスト時にKojectを使う方法について紹介します。 |
| 69 | + |
| 70 | +例えば、このような依存関係を持つコードがあったとします。 |
| 71 | + |
| 72 | +```kotlin |
| 73 | +interface Api |
| 74 | + |
| 75 | +@Provides |
| 76 | +@Binds |
| 77 | +class ApiImpl: Api |
| 78 | + |
| 79 | +interface SampleRepository |
| 80 | + |
| 81 | +@Provide |
| 82 | +@Binds |
| 83 | +class SampleRepositoryImpl( |
| 84 | + private val api: Api |
| 85 | +): SampleRepository |
| 86 | + |
| 87 | +@Provides |
| 88 | +class SampleController( |
| 89 | + private val repository: SampleRepository |
| 90 | +) |
| 91 | +``` |
| 92 | + |
| 93 | +この`SampleController`に対して、テストコードを作成してみましょう。<KojectRunTest/>を使うことで、テスト用のDIコンテナを起動してテストすることができます。 |
| 94 | +その後、<Inject/>関数を使って、依存が解決されたクラスを取得します。 |
| 95 | + |
| 96 | +```kotlin |
| 97 | +class SampleControllerTest() { |
| 98 | + @Test |
| 99 | + fun test() = Koject.runTest { |
| 100 | + val controller = inject<SampleController>() // can be injected |
| 101 | + } |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +Apiクラスをモックに差し替えたい場合、以下のように<TestProvides/>を使って上書きします。 |
| 106 | +これにより、テスト時は`MockApi`が使われ、本番環境では`ApiImpl`が使われます。 |
| 107 | + |
| 108 | +```kotlin |
| 109 | +@TestProvides |
| 110 | +@Binds |
| 111 | +class MockApi: Api |
| 112 | +``` |
| 113 | + |
| 114 | +テストには追加の依存関係の設定が必要です。 |
| 115 | +詳しくは、以下のドキュメントを参照してください。 |
| 116 | + |
| 117 | +* [テスト(共通)](/docs/test) |
| 118 | +* [Androidテスト](/docs/android/tests) |
| 119 | +* [iOSテスト](/docs/ios/tests) |
| 120 | + |
| 121 | +## UIテストでKojectを使う |
| 122 | + |
| 123 | +KojectはUIテスト等のintegrationテストでも利用することができます。 |
| 124 | + |
| 125 | +### Android UIテスト |
| 126 | + |
| 127 | +Androidアプリでは、実機もしくはエミュレータを使った[instrumentedテスト](https://developer.android.com/training/testing/instrumented-tests)を作成するか、[Robolectoric](https://robolectric.org/)を利用することで、UIテストを実行します。 |
| 128 | + |
| 129 | +UIテストでテスト用DIコンテナを利用するには、アプリケーションクラスで呼んでいる<KojectStart/>を<KojectStartTest/>に置き換える必要があります。 |
| 130 | +アプリケーションクラスの置き換えにはカスタム`Runner`を作成してください。 |
| 131 | + |
| 132 | +```kotlin |
| 133 | +class TestApplication : Application() { |
| 134 | + override fun onCreate() { |
| 135 | + super.onCreate() |
| 136 | + |
| 137 | + Koject.startTest { |
| 138 | + application(this@TestApplication) |
| 139 | + } |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | +```kotlin |
| 144 | +class TestRunner : AndroidJUnitRunner() { |
| 145 | + override fun newApplication( |
| 146 | + classLoader: ClassLoader?, |
| 147 | + className: String?, |
| 148 | + context: Context? |
| 149 | + ): Application { |
| 150 | + return super.newApplication(classLoader, TestApplication::class.java.name, context) |
| 151 | + } |
| 152 | +} |
| 153 | +``` |
| 154 | +
|
| 155 | +以下ドキュメントでより詳しい詳しい説明を行っています。 |
| 156 | +
|
| 157 | +* [Androidテスト](/docs/android/tests) |
| 158 | +
|
| 159 | +
|
| 160 | +### iOS UIテスト |
| 161 | +
|
| 162 | +Kojectは[Kotlin Multiplatform Mobile](https://kotlinlang.org/lp/mobile/)に対応しており、iOSでも利用することができます。 |
| 163 | +
|
| 164 | +iOSのUIテストは[XCTest](https://developer.apple.com/documentation/xctest)を利用し、Swiftで記述します。 |
| 165 | +以下のようにテスト時用の分岐を作成し、テスト用のDIコンテナを開始してください。 |
| 166 | +
|
| 167 | +```swift |
| 168 | +import SwiftUI |
| 169 | +import shared |
| 170 | + |
| 171 | +@main |
| 172 | +struct MyApp: App { |
| 173 | + init() { |
| 174 | + #if DEBUG |
| 175 | + let isTesting = CommandLine.arguments.contains("TESTING") |
| 176 | + if isTesting { |
| 177 | + KojectHelper.shared.startTest() |
| 178 | + } else { |
| 179 | + KojectHelper.shared.start() |
| 180 | + } |
| 181 | + #else |
| 182 | + KojectHelper.shared.start() |
| 183 | + #endif |
| 184 | + } |
| 185 | + |
| 186 | + var body: some Scene { |
| 187 | + /* ... */ |
| 188 | + } |
| 189 | +} |
| 190 | +``` |
| 191 | +```swift |
| 192 | +import XCTest |
| 193 | + |
| 194 | +final class UITests: XCTestCase { |
| 195 | + let app = XCUIApplication() |
| 196 | + |
| 197 | + func testSome() { |
| 198 | + app.launchArguments = ["TESTING"] |
| 199 | + app.launch() |
| 200 | + |
| 201 | + /* ... */ |
| 202 | + } |
| 203 | +} |
| 204 | +``` |
| 205 | +
|
| 206 | +iOSでKojectを利用する詳しい情報は、以下ドキュメントから確認できます。 |
| 207 | +
|
| 208 | +* [iOS(KMM)](/docs/ios/basic) |
| 209 | +* [iOSテスト](/docs/ios/tests) |
| 210 | +
|
| 211 | +## 推移的な依存関係の収集 |
| 212 | +Koject v1.3.0のもう一つの重要な変更点は、gradleマルチモジュールを使った場合の依存関係の収集が改善されたことです。 |
| 213 | +
|
| 214 | +Koject v1.2.0以前は、配布している全ての依存関係はappモジュールから直接参照できる必要がありました。 |
| 215 | +
|
| 216 | + |
| 217 | +
|
| 218 | +Koject v1.3.0からは推移的な依存関係の収集に対応したため、直接参照できる必要はありません。 |
| 219 | +
|
| 220 | + |
| 221 | +
|
| 222 | +有効化には、以下のようにモジュール名を指定する必要があります。 |
| 223 | +
|
| 224 | +```diff title="build.gradle" |
| 225 | +dependencies { |
| 226 | + implementation("com.moriatsushi.koject:koject-core:1.3.0-beta02") |
| 227 | + ksp("com.moriatsushi.koject:koject-processor-lib:1.3.0-beta02") |
| 228 | +} |
| 229 | + |
| 230 | ++ ksp { |
| 231 | ++ arg("moduleName", project.name) |
| 232 | ++ } |
| 233 | +``` |
| 234 | +
|
| 235 | +詳細は[セットアップドキュメント](/docs/setup)を確認してください。 |
| 236 | +
|
| 237 | +## Kojectを使って快適な開発を |
| 238 | +テスト時もDIコンテナを利用する有用性について紹介しました。 |
| 239 | +Kojectを利用することで、部分的に依存関係を差し替え、すぐにテストを開始することができます。 |
| 240 | +また、gradleマルチモジュールのサポートも強化されました。 |
| 241 | +
|
| 242 | +KojectはDIコンテナとして基本的な機能を全て揃えており、今すぐ利用することができます。 |
| 243 | +なにか問題があれば、いつでも[Issue](https://github.com/mori-atsushi/koject/issues)から教えて下さい。 |
0 commit comments