Types of Test Doubles
Types of Test Doubles
A test double is any object that stands in for a real dependency during testing. There are five common types:
Dummy
A dummy is passed around but never actually used. It satisfies a parameter requirement without contributing to the test.
// Dummy: we pass a user but never interact with it
val dummyUser = User(id = "0", name = "Dummy")
val viewModel = OrderViewModel(dummyUser, orderRepository)
// Only orderRepository interactions matter in this test
Stub
A stub provides predetermined responses to calls. It does not verify behavior — it only sets up state.
// Stub: always returns a fixed list
every { productRepository.getProducts() } returns listOf(
Product(id = "1", name = "Widget"),
Product(id = "2", name = "Gadget")
)
Spy
A spy is a real object that records interactions for later verification. You can still call real methods on it.
val realRepo = RealProductRepository(dao)
val spyRepo = spyk(realRepo)
viewModel.loadProducts()
verify { spyRepo.getProducts() } // Verify it was called
// But it actually hit the real DAO
Mock
A mock is a fully fake object with pre-programmed behavior AND built-in verification. Mocks are both stubs and spies.
val mockRepo = mockk<ProductRepository>()
every { mockRepo.getProducts() } returns emptyList()
viewModel.loadProducts()
verify { mockRepo.getProducts() }
Fake
A fake is a lightweight, working implementation of an interface. It behaves like the real thing but is simpler and faster.
class FakeProductRepository : ProductRepository {
private val products = mutableListOf<Product>()
override suspend fun getProducts(): List<Product> = products.toList()
override suspend fun getProduct(id: String): Product? =
products.find { it.id == id }
fun addProduct(product: Product) {
products.add(product)
}
}
Fakes are the highest-fidelity test double. They exercise the same code paths as the real implementation.
Fakes vs Mocks: When to Use Which
Fakes vs Mocks: When to Use Which
The choice between fakes and mocks is not arbitrary. Each has a clear role.
Use Fakes When
- Testing integration between layers. A fake repository with an in-memory database tests the same code paths as production.
- The dependency has complex state. A fake cart that tracks items, calculates totals, and applies discounts is more realistic than a mock.
- You want high confidence. Fakes catch bugs that mocks miss because they exercise real logic.
Use Mocks When
- The dependency is a boundary. External APIs, network clients, and third-party SDKs are best mocked because you cannot replicate their behavior easily.
- Testing interactions. Mocks shine when you need to verify that a method was called with specific arguments.
- The dependency is slow or flaky. A database query or network call is better mocked in unit tests.
In-Memory Database Fake
Room provides a simple way to create an in-memory database for tests:
@Before
fun setup() {
val context = ApplicationProvider.getApplicationContext<Context>()
database = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
dao = database.productDao()
}
@After
fun teardown() {
database.close()
}
This is a real Room database running in memory. It tests actual SQL queries, not stubbed responses.
Repository Fake Pattern
class FakeProductRepository : ProductRepository {
private val products = mutableMapOf<String, Product>()
private var shouldThrow = false
override suspend fun getProducts(): List<Product> {
if (shouldThrow) throw IOException("Network error")
return products.values.toList()
}
override suspend fun getProduct(id: String): Product? {
if (shouldThrow) throw IOException("Network error")
return products[id]
}
fun addProduct(product: Product) {
products[product.id] = product
}
fun setShouldThrow(value: Boolean) {
shouldThrow = value
}
}
This fake supports both success and error scenarios without MockK overhead.
Quiz
1. What is the key difference between a fake and a mock?
2. When should you prefer a fake over a mock?
3. What does Room.inMemoryDatabaseBuilder create?
4. What is a dummy in testing?
Flashcards
Question
What are the five types of test doubles?
Click to reveal answer
Answer
Dummy (passed but unused), Stub (predetermined responses), Spy (records interactions), Mock (pre-programmed + verified), Fake (working implementation).
Question
When should you use a mock instead of a fake?
Click to reveal answer
Answer
When testing external API boundaries, when you need to verify exact method calls, or when the dependency is too complex to fake realistically.
Question
What is the advantage of an in-memory Room database for tests?
Click to reveal answer
Answer
It exercises real SQL queries and DAO logic without disk I/O, making tests both realistic and fast.
Question
Why are fakes considered higher fidelity than mocks?
Click to reveal answer
Answer
Fakes have real logic that exercises the same code paths as production. Mocks only return canned responses and miss bugs in how the code uses the dependency.
Revision Notes
Key Takeaways
- 1. Fakes have working implementations, mocks have pre-programmed behavior
- 2. Use fakes for stateful dependencies like databases and repositories
- 3. Use mocks for external API boundaries where you cannot replicate behavior
- 4. In-memory Room databases exercise real SQL without disk I/O
- 5. A good fake with error simulation methods covers both success and failure paths
Interview Tips
- • Explain the difference between fakes and mocks with a concrete example
- • Discuss when you would choose a fake over a mock for a repository
- • Know how to set up Room.inMemoryDatabaseBuilder for tests
- • Be ready to describe a fake you built for a real project
Cheat Sheet
Test Doubles Cheat Sheet
Types:
- Dummy: Passed, never used
- Stub: Pre-programmed responses only
- Spy: Real object + records interactions
- Mock: Pre-programmed + verified
- Fake: Simplified working implementation
Decision Guide:
- Complex state (DB, repo) -> Fake
- External API boundary -> Mock
- Verify specific call -> Mock
- High confidence needed -> Fake
- Simple dependency -> Stub
In-Memory Room:
Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.allowMainThreadQueries()
.build()
Fake Repository Pattern:
- Mutable internal state
- setShouldThrow() for error paths
- Real method logic (filtering, sorting)