Los Test Doubles son objetos sustitutos utilizados en las pruebas unitarias en lugar de dependencias reales. El término fue introducido por Gerard Meszaros en el libro “xUnit Test Patterns” (2007) como un concepto general para Mock, Stub, Fake, Spy y Dummy. Según Martin Fowler (2024), los Test Doubles permiten aislar el componente bajo prueba de su entorno, haciendo que las pruebas sean deterministas, rápidas e independientes de servicios externos.
Puntos clave
Test Doubles es un término de la industria automotriz (dobles de riesgo), trasladado al desarrollo de software. Así como un doble de riesgo reemplaza a un actor en una escena peligrosa, un Test Double reemplaza un componente real en un escenario de prueba. Esto es necesario cuando la dependencia real no está disponible, es lenta, no determinista o tiene efectos secundarios.
El concepto de Test Double abarca cinco tipos específicos, cada uno resolviendo su propia tarea. La tipología de Meszaros es canónica y se utiliza en todas las guías modernas de pruebas. La diferencia entre los tipos radica en el grado de control y verificación: desde el simple llenado de parámetros (Dummy) hasta la verificación completa de secuencia de llamadas (Mock).
El propósito principal de los Test Doubles es aislar el módulo bajo prueba. En el desarrollo móvil, las dependencias reales incluyen servidores API, bases de datos, sistemas de archivos, sensores del dispositivo y servicios del sistema (LocationManager, Camera, Bluetooth). Usar estos componentes directamente hace que las pruebas sean lentas, frágiles y dependientes del entorno. Según Google Testing Blog (2023), las pruebas unitarias bien aisladas se ejecutan en milisegundos, mientras que las pruebas de integración se ejecutan en segundos y minutos.
La clasificación de Gerard Meszaros incluye cinco tipos de Test Doubles, que se diferencian en comportamiento y propósito. Comprender la diferencia entre ellos es la base de las pruebas unitarias competentes.
Dummy es un objeto que se pasa al método bajo prueba pero nunca se utiliza. Dummy solo es necesario para satisfacer la firma del método. En Kotlin, esto suele ser null, emptyList() o un objeto con stubs. Dummy no debe contener ninguna lógica — si se llama, la prueba debe fallar.
Fake es una implementación simplificada pero funcional de una interfaz. A diferencia de Mock y Stub, Fake contiene lógica de negocio real, pero en forma simplificada. Un ejemplo clásico es InMemoryUserRepository, que almacena datos en un HashMap en lugar de una base de datos. Fake se usa cuando se necesita probar lógica que depende del estado, pero sin la sobrecarga de la infraestructura real.
| Tipo | Propósito | Ejemplo |
|---|---|---|
| Dummy | Llenar un parámetro | null, objeto vacío |
| Fake | Implementación simplificada funcional | InMemoryRepository |
| Stub | Devolver un valor fijo | when(api.getUser()).thenReturn(user) |
| Spy | Registrar llamadas para verificación | verify(spy).save(user) |
| Mock | Verificar interacción | verify(mock).sendEmail(email) |
Stub devuelve valores predefinidos para llamadas específicas. Stub no verifica si fue llamado — simplemente proporciona datos. En Mockito, Stub se crea mediante when(method).thenReturn(value). Stub es ideal para pruebas cuando se necesita que una dependencia devuelva un valor específico, pero el hecho de la llamada en sí no es importante.
Spy es un envoltorio alrededor de un objeto real que registra todas las llamadas para su verificación posterior. A diferencia de Mock, Spy delega las llamadas al objeto real pero permite verificar que ocurrieron. En Mockito, Spy se crea mediante spy(realObject). Spy es útil para mocking parcial, cuando se quiere usar un objeto real pero verificar algunas llamadas.
Mock es un objeto con expectativas de llamada predefinidas. Mock verifica que métodos específicos fueron llamados con argumentos específicos y en un orden específico. A diferencia de Stub, Mock se centra en la verificación de comportamiento en lugar de la devolución de datos. Mock es el tipo de Test Double más potente y más utilizado en el desarrollo móvil.
La diferencia entre Mock y Stub a menudo causa confusión incluso entre desarrolladores experimentados. La principal diferencia está en el propósito: Stub verifica el estado (state verification), Mock verifica el comportamiento (behavior verification).
Stub responde a la pregunta: “¿devolvió el código el resultado correcto?”. Mock responde a la pregunta: “¿llamó el código a los métodos correctos con los argumentos correctos?”. En el desarrollo móvil, Stub se usa cuando el importa el resultado (ej., datos de un repositorio), mientras que Mock se usa cuando importan los efectos secundarios (ej., enviar correo electrónico, escribir en una base de datos).
// Stub: verificación de estado
every { repository.getUsers() } returns listOf(user)
val result = useCase.getUsers()
assertEquals(1, result.size)
// Mock: verificación de comportamiento
every { analytics.logEvent("purchase") } returns Unit
useCase.purchase(item)
verify { analytics.logEvent("purchase") }
Ejemplos prácticos de los cinco tipos de Test Doubles en Kotlin usando MockK — la biblioteca de mocking más popular para proyectos Android.
class InMemoryUserRepository : UserRepository {
private val store = mutableMapOf<String, User>()
override fun save(user: User) {
store[user.email] = user
}
override fun findByEmail(email: String): User? {
return store[email]
}
}
class RegisterUseCaseTest {
private val api = mockk<AuthApi>()
private val repo = spyk(InMemoryUserRepository())
private val useCase = RegisterUseCase(api, repo)
fun `register user successfully`() = runTest {
// Stub: devolver respuesta fija de API
coEvery { api.register("test@test.com") } returns AuthResult.Success("token123")
val result = useCase.execute("test@test.com")
// Verify: verificar que el usuario se guardó
verify { repo.save(any()) }
assertTrue(result.isSuccess())
}
}
data class Logger(val appContext: Context, val format: FormatType)
fun `test logger with dummy context`() {
// Dummy: Context no se usa dentro de Logger
val dummyContext = mockk<Context>()
val logger = Logger(dummyContext, FormatType.JSON)
assertEquals(FormatType.JSON, logger.format)
}
La elección del tipo de Test Double depende de qué se está probando exactamente: estado, comportamiento o integración. En el desarrollo móvil para Android e iOS, se han establecido las siguientes recomendaciones.
Al probar ViewModel, use Mock para dependencias que producen efectos secundarios (repositorios, analíticas, navegación) y Stub para dependencias que devuelven datos (clientes API, ContentProvider). Esto permite verificar que el ViewModel maneja correctamente tanto los escenarios exitosos como los de error.
A nivel de Repository, prefiera Fake (implementaciones de base de datos en memoria) y Stub (respuestas fijas de API). Fake permite probar la lógica de almacenamiento en caché y el modo sin conexión sin configurar SQLite. Stub simula varios estados HTTP: 200, 404, 500, timeout.
El uso incorrecto de Test Doubles es una de las causas más comunes de pruebas frágiles que se rompen con cada refactorización.
El error más común es mockearlo todo. Si cada dependencia en una prueba se reemplaza con un Mock, la prueba deja de verificar el comportamiento real. Mock solo debe usarse para dependencias externas (red, base de datos, sistema de archivos, servicios del sistema). Los componentes internos de la aplicación (Value Object, data class, utilidades simples) no deben reemplazarse.
El segundo error es crear un Mock sin definir expectativas. Si se llama a un método sin every / when, Mock devuelve un valor predeterminado (null, 0, false). Esto puede llevar a pruebas falsas positivas, donde Mock devuelve silenciosamente null y la prueba interpreta esto como un comportamiento correcto.
El tercer error es verificar cada llamada de cada Mock. Verify solo debe usarse para llamadas que son críticamente importantes desde la perspectiva de la lógica de negocio. La verificación excesiva hace que las pruebas sean frágiles: cambiar el orden de las llamadas en el código de producción rompe las pruebas sin cambiar el comportamiento.
Preguntas frecuentes
Stub devuelve datos y verifica el estado (qué se devolvió), mientras que Mock verifica el comportamiento (qué métodos se llamaron). Stub = “devuelve X”, Mock = “verifica que se llamó a Y con el argumento Z”. En las pruebas reales, un mismo objeto a menudo actúa como Stub y Mock simultáneamente.
Fake es preferible a Mock cuando se prueba lógica que depende del estado: almacenamiento en caché, modo sin conexión, transacciones. Fake (implementación en memoria) permite probar estos escenarios sin llamadas verify frágiles. Mock es más adecuado para verificar el envío de datos: analíticas, push, email.
Para proyectos Android en Kotlin, se recomienda MockK. Admite corrutinas, funciones suspend, clases selladas y funciones de extensión sin configuración adicional. Para proyectos Java, Mockito sigue siendo el estándar — la biblioteca más popular con documentación extensa.
Para probar Kotlin Flow, use la biblioteca Turbine junto con MockK. Turbine simplifica la verificación de emisiones de Flow: puede verificar el orden de los valores, la finalización del flujo y las excepciones. Stub para Flow devuelve flowOf(value), Mock verifica que se recolectó el Flow.
Sí, pero a nivel de respuestas API, no de componentes de UI. Las bibliotecas MockWebServer (OkHttp) y WireMock permiten simular respuestas HTTP en pruebas de UI. Los componentes de UI en sí (Compose, SwiftUI Views) no deben reemplazarse — su comportamiento se prueba mediante pruebas de captura de pantalla y Espresso.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también