La diferencia fundamental
Un Mock es un objeto generado por un framework (Mockk, Mockito) que registra todas las interacciones y permite configurar qué retorna. No tiene comportamiento propio — solo hace lo que le decís.
Un Fake es una implementación real pero simplificada de una interfaz. Tiene su propia lógica interna — generalmente usando estructuras en memoria en lugar de una base de datos o red real.
// MOCK — configurado por test, sin lógica propia
val mockRepo = mockk<ProductoRepository>()
coEvery { mockRepo.getProductos() } returns listOf(p1, p2)
coEvery { mockRepo.guardar(any()) } just Runs
// FAKE — implementación real en memoria
class FakeProductoRepository : ProductoRepository {
private val productos = mutableListOf<Producto>()
private var errorSimulado: Exception? = null
override suspend fun getProductos(): List<Producto> {
errorSimulado?.let { throw it }
return productos.toList()
}
override suspend fun guardar(producto: Producto) {
errorSimulado?.let { throw it }
productos.removeIf { it.id == producto.id }
productos.add(producto)
}
// Helpers para configurar el estado desde el test
fun setProductos(lista: List<Producto>) { productos.clear(); productos.addAll(lista) }
fun setError(e: Exception) { errorSimulado = e }
fun clearError() { errorSimulado = null }
}
Cuándo usar Fake — la mayoría de los casos
Los Fakes son superiores en la mayoría de los casos porque:
- Los tests son más legibles: no hay
coEvery { ... } returns ...en cada test. El estado se configura directamente. - Comportamiento real: si guardás y después consultás, el Fake retorna lo que guardaste. Un mock necesita configurarse para cada caso.
- Mantenimiento más fácil: cuando la interfaz cambia, el Fake falla en compilación. Un mock puede quedar desincronizado silenciosamente.
- Reutilizable en todos los tests: el FakeRepository se usa en tests del ViewModel, del Use Case, y de otros componentes. Un mock se configura de cero en cada test.
// Con Fake — tests limpios y claros
class ProductosViewModelTest {
private val fakeRepo = FakeProductoRepository()
private val viewModel = ProductosViewModel(fakeRepo)
@Test
fun `cargar muestra los productos del repositorio`() = runTest {
fakeRepo.setProductos(listOf(p1, p2, p3))
viewModel.cargar()
assertThat(viewModel.uiState.value.productos).hasSize(3)
}
@Test
fun `eliminar producto lo quita de la lista`() = runTest {
fakeRepo.setProductos(listOf(p1, p2))
viewModel.eliminar(p1.id)
assertThat(viewModel.uiState.value.productos).containsExactly(p2)
}
}
Cuándo usar Mock — casos específicos
Los mocks tienen ventaja cuando:
- Necesitás verificar interacciones: "¿se llamó a
analytics.logEvento()exactamente una vez?" - La dependencia es difícil de hacer Fake: un servicio con decenas de métodos donde solo usás dos.
- Querés testear que una excepción se maneja correctamente: configurar que lanze una IOException puntual es más limpio con un mock.
// Mock para verificar interacciones con Analytics
val mockAnalytics = mockk<AnalyticsTracker>(relaxed = true)
val viewModel = ProductosViewModel(fakeRepo, mockAnalytics)
@Test
fun `seleccionar producto loggea el evento en analytics`() = runTest {
viewModel.seleccionar(p1)
verify(exactly = 1) {
mockAnalytics.logEvento("producto_seleccionado", withArg {
it["producto_id"] == p1.id.toString()
})
}
}
// Mock para inyectar un error específico sin mantener esa lógica en el Fake
@Test
fun `error de autenticacion muestra mensaje de sesion expirada`() = runTest {
val mockRepo = mockk<ProductoRepository>()
coEvery { mockRepo.getProductos() } throws UnauthorizedException()
val vm = ProductosViewModel(mockRepo, mockk(relaxed = true))
vm.cargar()
assertThat(vm.uiState.value.error).contains("sesión expirada")
}
Construir un FakeRepository completo
// Un FakeRepository bien construido:
class FakeProductoRepository : ProductoRepository {
// Estado interno — mutable desde los tests
private val _productos = MutableStateFlow<List<Producto>>(emptyList())
private var errorEnGetProductos: Exception? = null
private var errorEnGuardar: Exception? = null
private var delayMs: Long = 0L
// Implementación de la interfaz
override fun getProductosFlow(): Flow<List<Producto>> = _productos
override suspend fun getProductos(): List<Producto> {
if (delayMs > 0) delay(delayMs)
errorEnGetProductos?.let { throw it }
return _productos.value
}
override suspend fun guardar(producto: Producto) {
errorEnGuardar?.let { throw it }
val lista = _productos.value.toMutableList()
lista.removeIf { it.id == producto.id }
lista.add(producto)
_productos.value = lista
}
override suspend fun eliminar(id: Int) {
_productos.value = _productos.value.filter { it.id != id }
}
override suspend fun buscar(query: String): List<Producto> {
return _productos.value.filter { it.nombre.contains(query, ignoreCase = true) }
}
// Helpers para configurar el estado desde los tests:
fun setProductos(lista: List<Producto>) { _productos.value = lista }
fun agregarProducto(p: Producto) { _productos.value = _productos.value + p }
fun setErrorEnGet(e: Exception?) { errorEnGetProductos = e }
fun setErrorEnGuardar(e: Exception?) { errorEnGuardar = e }
fun simularDelay(ms: Long) { delayMs = ms }
fun reset() {
_productos.value = emptyList()
errorEnGetProductos = null
errorEnGuardar = null
delayMs = 0
}
}
El Fake vive en src/test/, no en src/main/El FakeRepository es código de test — no debe incluirse en el APK de producción. Poné todos los Fakes en src/test/java/. Si necesitás compartir un Fake entre tests unitarios e instrumentados, poné el código común en un módulo :core:testing.
Fake de fuente de datos — cuando el Fake tiene estado reactivo
// Fake que simula un DAO de Room con StateFlow
class FakeProductoDao : ProductoDao {
private val _productos = MutableStateFlow<List<ProductoEntity>>(emptyList())
override fun observarTodos(): Flow<List<ProductoEntity>> = _productos
override suspend fun insertarTodos(productos: List<ProductoEntity>) {
_productos.value = productos
}
override suspend fun borrarTodos() {
_productos.value = emptyList()
}
// El test puede simular que llegan nuevos datos "desde Room"
fun emitirProductos(lista: List<ProductoEntity>) {
_productos.value = lista
}
}
// Usar en test de repositorio:
@Test
fun `cuando Room actualiza, el repositorio emite nuevos datos`() = runTest {
val fakeDao = FakeProductoDao()
val repo = ProductoRepositoryImpl(mockApi, fakeDao)
repo.getProductosFlow().test {
// Estado inicial vacío
assertThat(awaitItem()).isEmpty()
// Simular que Room recibe datos nuevos
fakeDao.emitirProductos(listOf(entidad1, entidad2))
// El repo debe emitir los datos transformados
val productos = awaitItem()
assertThat(productos).hasSize(2)
cancelAndIgnoreRemainingEvents()
}
}
El glosario completo — Stub, Spy, Dummy
# Los "test doubles" son todos los objetos que reemplazan dependencias reales:
# DUMMY — se pasa como argumento pero nunca se usa
# Ej: un Logger que no hace nada en tests
# STUB — retorna valores fijos sin comportamiento real
# Mockk con every { } retornando un valor fijo
# FAKE — implementación simplificada pero funcional
# FakeRepository con lista en memoria
# MOCK — registra interacciones y verifica que se llamó correctamente
# mockk() con verify { }
# SPY — wrapper sobre un objeto real que intercepta algunas llamadas
# spyk(repositorioReal) para sobreescribir solo un método
# En la práctica cotidiana:
# → Usás Fakes para dependencias de datos (repositorios, DAOs, APIs)
# → Usás Mocks para verificar side effects (analytics, logging, navegación)
# → Usás Stubs inline para casos simples que no justifican un Fake