Testing with Fake Dependencies

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:

src/test/.../CatalogViewModelTest.kt: a ViewModel tested against a fake (excerpt)Kotlin
/** 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:

Output of 142
> 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.