こんにちは。
現在、業務でAndroidのUIテストを取り扱っています。
その中で、「テストケース毎にHiltのDI設定を切り替えることができないか?」と疑問に思い調査しました。
備忘録としてまとめていきます。
テスト時にDI設定を切り替える
アプローチは2種類あります。
- 統合テスト全体で切り替える
- テストクラス毎に切り替える
まずは両者に共通で必要な事項を説明します。
共通事項
公式のリファレンスに載っている中で関係あるものを抽出しています。
詳しく知りたい方はリンク先を見ていただければ。
テスト用の依存関係追加
モジュールの依存関係に以下を追加します。
バージョンは適宜読み替えてください。
dependencies {
// For instrumented tests.
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.44'
// ...with Kotlin.
kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.44'
}
UIテストの設定
テストクラスの先頭に@HiltAndroidTest
アノテーションを付与してください。
これがあると、各テストがHiltコンポーネントを生成します。
テストクラスにHiltAndroidRule
を追加する必要もあります。これは、コンポーネントの状態を管理し、テストのインジェクションに使用されるものです。
@HiltAndroidTest
class SampleTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
// UI tests here.
}
テストランナーの設定
統合テストでHiltテストアプリを使用するには、新しいテストランナーを設定する必要があります。
これにより、プロジェクト内のすべての統合テストでHiltが機能するようになります。次の手順を行います。
- androidTest フォルダに AndroidJUnitRunner を拡張するカスタムクラスを作成します。
- newApplication 関数をオーバーライドし、生成される Hilt テストアプリの名前を渡します。
// A custom runner to set up the instrumented application class for tests.
class CustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
次に、このテストランナーをモジュールのGradleファイルで設定します。次のように完全なクラスパスを使用してください。
android {
defaultConfig {
// Replace com.example.android.dagger with your class path.
testInstrumentationRunner "com.example.android.dagger.CustomTestRunner"
}
}
置き換える前のDI設定(モジュール)
今回は以下の様なモジュールを想定します。
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Singleton
@Binds
abstract fun bindSampleRepository(
impl: SampleRepositoryImpl
): SampleRepository
@Singleton
@Binds
abstract fun bindTestRepository(
impl: TestRepositoryImpl
): TestRepository
}
統合テスト全体で置き換える
androidTest
フォルダ配下に以下の様なクラスを作成します。
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RepositoryModule::class]
)
abstract class FakeRepositoryModule {
@Singleton
@Binds
abstract fun bindSampleRepository(
impl: FakeSampleRepository
): SampleRepository
@Singleton
@Binds
abstract fun bindTestRepository(
impl: FakeTestRepository
): TestRepository
}
これにより、統合テスト内ではRepositoryModule
で定義した設定の代わりにFakeRepositoryModule
で定義した設定に基づき依存性の注入が行われます。
単一のテストクラス内でのみ置き換える
テストクラスの先頭に
@UninstallModule
アノテーションを付与することで、そのテストクラス内で本番環境のDI設定を無視することができます。
@UninstallModules(RepositoryModule::class)
@HiltAndroidTest
class SampleTest { ... }
次に、テストクラス内でDI設定を追加する必要があります。
これは2種類方法があります。
テストクラス内にモジュールを作成する
@UninstallModules(RepositoryModule::class)
@HiltAndroidTest
class SampleTest {
@Module
@InstallIn(SingletonComponent::class)
abstract class TestRepositoryModule {
@Singleton
@Binds
abstract fun bindSampleRepository(
impl: FakeSampleRepository
): SampleRepository
@Singleton
@Binds
abstract fun bindTestRepository(
impl: FakeTestRepository
): TestRepository
}
...
}
@BindValueアノテーションを利用する
テスト内でオブジェクトのモックをしたい場合などに便利です。
@UninstallModules(RepositoryModule::class)
@HiltAndroidTest
class SampleTest {
@BindValue
val mockSampleRepository = mockk<SampleRepository>()
// もちろんモックじゃなくてもよい
@BindValue
val fakeTestRepository: TestRepository = FakeTestRepository()
}
この方法で、単一のテストクラス内でのみ適用するモジュールを変更することができます。
なお、@UninstallModule
で無効にできるのは@InstallIn
のモジュールだけです。
前述の@TestInstallIn
モジュールをアンインストールしようとするとコンパイルエラーが発生します。
テストケース毎にDI設定を切り替えたい
上記の法方では、テスト全体や、テストクラス毎にDI設定を切り替えることができますが、テストケース毎に切り替えることはできません。
モジュール内にフラグとなる変数を定義し、フラグによって注入するインスタンス(あるいはそのプロパティ)を変更するようにすればよいのではと試しました。
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [RepositoryModule::class]
)
object TestRepositoryModule {
var fakePattern = 0
@Singleton
@Provide
fun providesSampleRepository(): SampleRepository {
return FakeSampleRepository(fakePattern)
}
@Singleton
@Provide
fun providesTestRepository(): TestRepository {
return FakeTestRepository(fakePattern)
}
}
class FakeSampleRepository @Inject constructor(
private val fakePattern: Int
): SampleRepository {
// fakePatternの値によって実装を変える
override fun method(): String {
return when(fakePattern) {
0 -> "zero"
1 -> "one"
2 -> "two"
else -> "error"
}
}
}
テストコードでは、各テストの開始時にテストケースに合わせてfakePatternを変更します。
@Test
fun methodTest_one() {
TestRepositoryModule.fakePattern = 1
// any tests...
}
この方法でテストケース毎にDI設定を切り替えることができました。
Singletonであっても各テストケースのたびにに依存性注入が行われるので、連続で2個以上のテストを実行する場合も大丈夫です。
依存性の注入は、はじめてそのオブジェクトが参照されるときに行われるので、必ず参照の前にfakePatternを変更してください。
例えば、@Before
アノテーションでテスト実行前のセットアップ時にオブジェクトの参照が行われないように注意してください。