Test doubles (mocks, stubs, fakes) replace real dependencies in tests so you can test one unit of code in isolation. Choosing the right type and knowing when to avoid them altogether is critical to a healthy test suite.
The Vocabulary
| Type | Description | When to use |
|---|---|---|
| Fake | A working implementation with simplified behavior (e.g., in-memory DB) | Most of the time — preferred |
| Stub | Returns hardcoded values for specific calls | Simple, one-off return values |
| Mock | Verifies that specific methods were called with specific arguments | Verifying side effects |
| Spy | Wraps a real object, overriding only specific methods | Rare — avoid usually |
Fakes: The Best Tool
A fake is a lightweight, fully functional replacement for a real dependency:
// Real interface
interface ArticleRepository {
suspend fun getArticle(id: String): Article?
suspend fun saveArticle(article: Article)
suspend fun deleteArticle(id: String)
}
// Fake: in-memory implementation — no Mockito, no mocking framework
class FakeArticleRepository : ArticleRepository {
private val articles = mutableMapOf<String, Article>()
override suspend fun getArticle(id: String) = articles[id]
override suspend fun saveArticle(article: Article) { articles[article.id] = article }
override suspend fun deleteArticle(id: String) { articles.remove(id) }
// Test helpers — not part of the interface
fun seedArticles(vararg articles: Article) { articles.forEach { this.articles[it.id] = it } }
fun allArticles() = articles.values.toList()
}
// Test using the fake
class ArticleViewModelTest {
private val repository = FakeArticleRepository()
private val viewModel = ArticleViewModel(repository)
@Test
fun `loading article shows correct state`() = runTest {
repository.seedArticles(Article("a1", "Hello World"))
viewModel.loadArticle("a1")
assertEquals("Hello World", viewModel.state.value.title)
}
}
Stubs with Mockito
// implementation("org.mockito.kotlin:mockito-kotlin:5.2.1")
@Test
fun `fetch returns articles from api`() = runTest {
val mockApi = mock<ArticleApi>()
// Stub: define what the mock returns for a specific call
whenever(mockApi.fetchArticles(page = 1)).thenReturn(listOf(testArticle))
val repository = ArticleRepository(api = mockApi, dao = FakeArticleDao())
val result = repository.getPage(1)
assertEquals(listOf(testArticle), result)
}
Mocks for Verifying Interactions
@Test
fun `analytics event sent on article view`() = runTest {
val mockAnalytics = mock<Analytics>()
val viewModel = ArticleViewModel(repository, analytics = mockAnalytics)
viewModel.onArticleOpened("a1")
// Verify the mock was called with specific arguments
verify(mockAnalytics).track("article_view", mapOf("id" to "a1"))
verifyNoMoreInteractions(mockAnalytics)
}
Hermetic Tests: No Real I/O
Hermetic tests never reach the real network, database, or file system. They're fast and deterministic:
// ❌ Not hermetic — depends on real network
@Test
fun `api returns articles`() = runTest {
val result = RealArticleApi().fetchArticles() // real HTTP call
assertTrue(result.isNotEmpty())
}
// ✅ Hermetic — uses MockWebServer or fake
@Test
fun `api parses article response correctly`() = runTest {
val server = MockWebServer().also {
it.enqueue(MockResponse().setBody("""[{"id":"a1","title":"Hello"}]"""))
it.start()
}
val api = Retrofit.Builder().baseUrl(server.url("/")).build().create(ArticleApi::class.java)
val result = api.fetchArticles()
assertEquals("Hello", result[0].title)
server.shutdown()
}
When to Avoid Mocks
- Don't mock the database: use Room's in-memory database instead. Mocking Room DAOs tests nothing useful and misses real SQL bugs.
- Don't mock your own code: if you're mocking a class you own, consider extracting an interface and using a fake.
- Don't mock data classes: just instantiate them with test values.
Key Takeaways
| Rule | Why |
|---|---|
| Prefer fakes over mocks | Fakes compile-check the interface; mocks can silently miss refactors |
| Use mocks for verifying side effects | Checking analytics events, notification sends — not return values |
| In-memory Room for database tests | Tests real SQL; Room.inMemoryDatabaseBuilder is fast |
MockWebServer for HTTP | Tests real JSON parsing; no network dependency |
| Hermetic = no real I/O | Fast, deterministic, no flakiness from network/disk state |