Mocks vs fakes vs stubs en testing móvil: ¿cuándo usas realmente cada uno?
Los stubs responden 'qué devolvió', los mocks responden 'se llamó', y los fakes son el único de los tres con estado real que funciona de verdad.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Mocks vs fakes vs stubs en testing móvil: ¿cuándo usas realmente cada uno?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Un término paraguas, tres herramientas de todos los días
"Doble de prueba" es el término paraguas — la taxonomía original de Gerard Meszaros en realidad nombra cinco: dummy, stub, spy, fake y mock —, pero el trabajo móvil del día a día solo necesita tres para razonar con claridad sobre una prueba: stub, fake y mock. La distinción que importa no es una trivia de vocabulario. Es si tu prueba está chequeando un valor que produjo tu código (testing basado en estado) o chequeando que tu código hizo una llamada en particular (testing basado en comportamiento), y cada doble existe para uno de esos dos trabajos.
#Stub: una respuesta enlatada, nada más
Un stub devuelve un valor pre-programado cuando se lo llama y nada más — a tu prueba no le importa cuántas veces se lo llamó ni en qué orden, solo que devolvió algo sobre lo cual podés construir una aserción. coEvery { api.currentWeather("Lima") } returns Weather(city = "Lima", sunny = true) es un stub: existe únicamente para llevar a tu ViewModel al estado sobre el cual querés afirmar. Recurrí a un stub cada vez que el valor de retorno del colaborador dispara una rama y la llamada en sí no es lo interesante.
#Fake: una implementación real, más chica
Un fake es una implementación real que funciona de verdad, solo que más liviana que la de producción — un mapa en memoria que hace de Room, una lista simple que hace de cola de red. A diferencia de un stub, un fake tiene estado real que persiste y evoluciona entre llamadas dentro de una prueba, lo cual lo convierte en la herramienta correcta cada vez que importa una secuencia: cachear un valor y después leerlo, sembrar varios registros y después filtrarlos, emitir "cargando" y después "éxito" desde el mismo colaborador a lo largo del tiempo. Un fake cuesta más escribirlo una vez, y después se reutiliza en cada prueba que necesite ese colaborador, mientras que un stub normalmente se reescribe por prueba.
#Mock: probar que una llamada ocurrió
Un mock existe para verificar comportamiento, no para devolver un valor que tu código necesite para seguir — programás una expectativa, ejercitás el código, y después afirmás que la interacción ocurrió: verify(exactly = 1) { tracker.track("checkout_completed", ...) }. Recurrí a un mock específicamente cuando no hay ningún valor de retorno sobre el cual afirmar y el hecho de la llamada es el comportamiento bajo prueba: eventos de analítica, breadcrumbs de reporte de crashes, el registro de un push token, un comando de navegación disparado exactamente una vez.
#Dónde se rompe esto
El modo de falla del sobre-mockeo es mockear un colaborador que tiene comportamiento real y testeable — un caché, un validador, un mapper — solo porque una librería de mocking lo hace fácil, y después afirmar que el mock fue llamado con argumentos específicos en un orden específico. Esa prueba ahora conoce la implementación de la colaboración, no el resultado, y se rompe ante cualquier refactor que no cambie el comportamiento en absoluto — exactamente la fragilidad que un equipo móvil no puede permitirse una vez que el código tiene cientos de ViewModels y pantallas.
#Nota de plataforma: iOS no trae una librería de mocking
Kotlin/MockK genera mocks dinámicos en tiempo de ejecución porque la JVM permite manipular el bytecode de clases open. El despacho estático de Swift y su preferencia por value types hacen ese enfoque mucho más difícil, así que XCTest no trae ningún framework de mocking incorporado — los equipos de iOS o escriben a mano fakes y stubs que conforman a un protocolo (el enfoque idiomático, y a menudo el mejor de todos modos) o recurren a una herramienta de generación de código como Mockolo o Cuckoo que produce conformancias mock en tiempo de compilación. Nombrar esa decisión en voz alta — "en iOS armaría a mano un fake que conforma al protocolo antes de recurrir a una librería de mocking" — ya es en sí una señal de fluidez con la plataforma.
#Ejemplo resuelto, lado a lado
interface WeatherApi {suspend fun currentWeather(city: String): Weather}interface AnalyticsTracker {fun track(event: String, params: Map<String, String> = emptyMap())}data class Weather(val city: String, val sunny: Boolean)// STUB -- una respuesta enlatada, no hace falta verificar la llamada.class WeatherViewModelStubTest {private val api = mockk<WeatherApi>()@Testfun `shows sunny banner when the api reports clear skies`() = runTest {coEvery { api.currentWeather("Lima") } returns Weather(city = "Lima", sunny = true)val viewModel = WeatherViewModel(api)viewModel.load("Lima")assertEquals(WeatherUiState.Sunny, viewModel.uiState.value)// Sin coVerify acá -- no nos importa CÓMO se llamó, solo qué devolvió.}}// FAKE -- una implementación real, más chica, con estado de verdad.class InMemoryWeatherApi : WeatherApi {private val cannedResponses = mutableMapOf<String, Weather>()fun seed(city: String, weather: Weather) {cannedResponses[city] = weather}override suspend fun currentWeather(city: String): Weather =cannedResponses[city] ?: error("No fixture seeded for $city")}// MOCK -- verificación de comportamiento: el punto de la prueba ES que la llamada ocurrió.class CheckoutAnalyticsTest {private val tracker = mockk<AnalyticsTracker>(relaxUnitFun = true)@Testfun `tracks checkout_completed exactly once with the order total`() {val checkout = CheckoutFlow(tracker)checkout.complete(orderTotal = 42.50)verify(exactly = 1) {tracker.track("checkout_completed", mapOf("total" to "42.50"))}}}
🧠 Check your understanding
0/3 · 0/3 answered1. Una prueba stubbea la llamada a un repositorio para que devuelva un valor fijo y nunca chequea si se invocó, ni cuántas veces. Ese doble está funcionando como:
2. Necesitás probar que un repositorio cachea correctamente un valor al escribir y devuelve el valor cacheado en una lectura posterior, sin golpear la red dos veces. ¿Cuál es el mejor doble?
3. ¿Por qué la cultura de testing en iOS se inclina hacia fakes escritos a mano en vez de una librería de mocking, a diferencia del testing típico en Kotlin/JVM?