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