Constructor injection pays off in tests: a JVM unit test hands CatalogViewModel a fake repository, a small working implementation, through its constructor. No emulator, Hilt or assets are involved:
/** A fake: real behavior, canned data, no delay, and a switch to make it fail. */
class FakeBookRepository(var books: List<Book>, var fail: Boolean = false) : BookRepository {
override suspend fun loadBooks(): List<Book> =
if (fail) throw IOException("offline") else books
}
...
@OptIn(ExperimentalCoroutinesApi::class)
class CatalogViewModelTest {
@Before fun setUp() = Dispatchers.setMain(UnconfinedTestDispatcher()) // viewModelScope
@After fun tearDown() = Dispatchers.resetMain()
...
@Test fun failureBecomesErrorAndRetryRecovers() = runTest {
val repo = FakeBookRepository(emptyList(), fail = true)
val vm = CatalogViewModel(repo) // plain constructor call, no Hilt
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) { vm.books.collect {} }
assertEquals(LoadState.Error("offline"), vm.books.value)
repo.fail = false
vm.refresh()
assertEquals(LoadState.Success(emptyList<Book>()), vm.books.value)
}
}Dispatchers.setMain replaces the main dispatcher behind viewModelScope, since the JVM has no Android main thread, and UnconfinedTestDispatcher runs coroutines at once. The backgroundScope collector matters: with WhileSubscribed(5_000), books stays Loading until someone subscribes. unitTests.isReturnDefaultValues = true in the app module makes Log.d return 0 on the JVM. ./gradlew testDebugUnitTest ran both tests:
> Task :app:testDebugUnitTest CatalogViewModelTest > failureBecomesErrorAndRetryRecovers PASSED CatalogViewModelTest > searchFiltersLoadedBooks PASSED BUILD SUCCESSFUL in 2m 7s
Prefer fakes to mocks (MockK 5,760 ) for your own interfaces. Tests that need the real Hilt graph (instrumented or Robolectric tests, Testing) swap modules instead. BookNest's test source set has a FakeDataModule annotated @TestInstallIn(components = [SingletonComponent::class], replaces = [DataModule::class]) that binds an in-memory repository, so every @HiltAndroidTest class, run with a HiltAndroidRule, gets the fake wherever the app would use AssetBookRepository. hilt-android-testing and kspTest(hilt-compiler) compiled it in the same run.