Spy (espía) — un objeto de prueba que envuelve una instancia real y registra información sobre cada llamada: qué métodos se invocaron, con qué argumentos y cuántas veces. A diferencia de un mock, un spy usa la implementación real del objeto envuelto — las llamadas pasan por el código real, y el spy solo registra los hechos. Después de ejecutar la prueba, el desarrollador revisa los registros del espía: “¿Se llamó al método sendAnalytics tres veces?”. Más información en la guía de pruebas de Android.
Puntos clave
Spy — es un envoltorio alrededor de un objeto real que intercepta todas las llamadas a métodos y las registra. La lógica real del objeto se ejecuta: si el método guarda datos, calcula un valor o realiza una solicitud — todo ocurre con normalidad. Adicionalmente, el spy registra metadatos: nombre del método, argumentos, número de llamadas, tiempo de ejecución. El término forma parte de la clasificación de Meszaros (2007) y se describe detalladamente en el artículo de Martin Fowler “Mocks Aren’t Stubs”.
Diferencia clave con un Mock — un mock reemplaza completamente el objeto con un stub de prueba; todos los métodos no hacen nada por defecto. Un spy envuelve un objeto existente: todos los métodos funcionan con normalidad por defecto, pero además se registran. Esta distinción es fundamental: un mock aísla el código probado de la realidad, mientras que un spy preserva la realidad y permite observarla. La elección entre ellos depende de lo que se esté probando.
Un spy es la elección correcta — si el código probado modifica el estado de un objeto real, y la prueba debe verificar tanto el resultado (estado) como asegurarse de que las llamadas se hicieron en el orden correcto. Un mock no es adecuado porque no ejecuta la implementación real. Un stub no es adecuado porque no registra llamadas. Un spy es el único test double que preserva la lógica real y proporciona información sobre las llamadas.
Mock — aislamiento completo. Si la prueba no debe depender de la implementación del objeto real (por ejemplo, una base de datos o un cliente de red), usa un mock. Un mock garantiza que ninguna llamada llegue al componente real. Es seguro y predecible. La desventaja: un mock no ejecuta lógica real, por lo que si el código probado depende de un valor de retorno, debe configurarse explícitamente mediante when/stub.
Spy — lógica real + observación. Si el código probado interactúa con un objeto cuya lógica es importante para la prueba, y no solo datos — usa un spy. Por ejemplo, un AnalyticsTracker que recopila eventos y los envía periódicamente. La prueba verifica que los eventos se añadieron al búfer y que, después del envío, el búfer se vació. Un mock no puede verificar esto porque no ejecuta la lógica real del tracker.
| Escenario | Spy | Mock |
|---|---|---|
| Lógica real necesaria | Sí | No (stub) |
| Verificación de llamadas | Sí (cantidad, argumentos) | Sí (cantidad, argumentos) |
| Stubbing parcial | Sí (unos métodos spy, otros stub) | No (todos los métodos son stubs) |
| Riesgo de efectos secundarios | Alto (código real) | Nulo |
| Velocidad | Menor (lógica real) | Mayor (stubs) |
| Legibilidad | Menor (más difícil entender qué es real) | Mayor (todo es explícito) |
Spy para todo — usar un spy en lugar de un mock para todas las pruebas es un error. Un spy ejecuta código real que puede tener efectos secundarios: escribir en un archivo, enviar HTTP, cambiar el estado global. Si el módulo probado llama a un método de un objeto spy que hace una solicitud HTTP, la prueba se convierte en una prueba de integración, no unitaria. Regla: si un spy envuelve un objeto con operaciones de E/S — ya no es una prueba unitaria. Usa mocks para aislar E/S y spies solo para objetos en memoria sin efectos externos.
Mockito.spy() — la forma clásica de crear un espía en proyectos Java/Kotlin. spy() toma un objeto real y devuelve un envoltorio. Todas las llamadas se delegan al objeto real por defecto y los resultados se registran. Después de la prueba, puedes usar verify() para comprobar la cantidad de llamadas y los argumentos. Para los métodos que deben devolver datos de prueba, se usa doReturn/when — esto se llama “partial mocking”.
class AnalyticsReporterTest {
private val realTracker = AnalyticsTracker()
private val spyTracker = Mockito.spy(realTracker)
fun test_event_tracked() {
val event = AnalyticsEvent("login")
spyTracker.track(event)
Mockito.verify(spyTracker).track(event)
assertEquals(1, spyTracker.getBufferedCount())
}
fun test_track_with_exception() {
Mockito.doThrow(RuntimeException("network"))
.when(spyTracker).flush()
spyTracker.track(AnalyticsEvent("login"))
assertTrue(spyTracker.hasPendingEvents())
}
}
MockK.spyk() — una alternativa para proyectos Kotlin con mejor soporte para corrutinas y clases selladas. MockK.spyk() crea un espía, análogo a Mockito.spy(). Soporta coVerify para funciones suspend y every para stubbing parcial. A diferencia de Mockito, MockK no soporta espías para clases final (todas las clases en Kotlin son final por defecto) — necesitas abrir la clase (open) o usar una interfaz.
class LoginUseCaseTest {
private val realRepo = UserRepository()
private val spyRepo = spyk(realRepo)
private val useCase = LoginUseCase(spyRepo)
fun test_login_calls_save() = runTest {
every { spyRepo.getUser(any()) } returns User("test")
val result = useCase.login("test", "pass")
coVerify { spyRepo.saveLoginTime(any()) }
assertTrue(result.isSuccess)
}
}
Stubbing parcial — una técnica potente pero peligrosa. Puedes crear un spy de un objeto y sobrescribir (stub) solo algunos métodos, dejando los demás reales. Ejemplo: un repositorio spy donde getUser() devuelve datos de prueba, mientras que saveUser() realmente guarda en una lista en memoria. Esto permite combinar las ventajas de los stubs (datos controlados) y los spies (lógica real). La desventaja: la legibilidad de la prueba se resiente — no es obvio qué métodos son reales y cuáles son stubs.
OCMock para Objective-C — una librería que soporta la creación de objetos spy mediante niceMock. OCMock intercepta las llamadas a métodos usando el runtime de Objective-C y las registra. Después de la prueba se llama a verify. OCMock soporta espías para cualquier objeto (todos los métodos en Objective-C son dinámicos), lo que le da ventaja sobre Swift, donde los espías solo son posibles mediante protocolos.
// Creación de un spy para un objeto real
AnalyticsTracker *realTracker = [[AnalyticsTracker alloc] init];
AnalyticsTracker *spy = [OCMockObject partialMockForObject:realTracker];
// Ejecución de la prueba
[spy trackEvent:@"login"];
// Verificación
[[spy verify] trackEvent:@"login"];
XCTAssertEqual([realTracker eventCount], 1);
Spy basado en protocolos en Swift — Swift no tiene reflexión de runtime como Objective-C, por lo que los espías se crean manualmente. Una estructura de prueba implementa un protocolo y, internamente, llama al objeto real mientras registra las llamadas simultáneamente. Esto requiere más código, pero es totalmente controlable y type-safe. Los espías manuales no requieren librerías externas ni usan runtime — todo se comprueba en tiempo de compilación.
protocol AnalyticsProtocol {
func trackEvent(name: String)
}
final class SpyAnalytics: AnalyticsProtocol {
private let real: AnalyticsProtocol
private var events: [String] = []
init(real: AnalyticsProtocol) {
self.real = real
}
func trackEvent(name: String) {
events.append(name)
real.trackEvent(name: name)
}
func verifyTracked(name: String) -> Bool {
return events.contains(name)
}
}
Cuándo usar OCMock vs spy manual — para código Objective-C, usa OCMock (menos boilerplate). Para Swift, se prefieren los espías manuales mediante protocolos. Un spy manual da control total sobre el registro de llamadas, no requiere reflexión y funciona con tipos valor (structs). El único inconveniente es que debes mantener el código de la clase spy sincronizado con el protocolo al añadir nuevos métodos.
Verificación de analítica — el caso de uso más común para spies. En código de producción, las llamadas de analítica están dispersas por toda la aplicación: inicio de sesión, cierre de sesión, compra, error. La prueba crea un spy de AnalyticsTracker, ejecuta un escenario (iniciar sesión, ver producto, añadir al carrito, comprar) y verifica que todos los eventos necesarios se enviaron en el orden correcto. Un mock no es adecuado porque AnalyticsTracker contiene lógica de almacenamiento en búfer y envío.
Temporizadores y planificadores — probar código que usa Handler (Android) o Timer (iOS) es difícil debido al tiempo real. Un spy de Scheduler registra qué tareas se planificaron y con qué retardo. La prueba crea un spy del Handler real, realiza una acción y verifica que Handler.postDelayed(runnable, delay) se llamó con el retardo correcto. La tarea real no se ejecuta — el spy intercepta y registra la llamada.
Registro y depuración — en producción, los registros pueden estar desactivados o escribirse en un archivo. Un spy de Logger registra todos los mensajes en una lista en memoria que la prueba verifica después de la ejecución. Esto permite comprobar que se escribe el mensaje correcto en caso de error sin saturar la consola. Los spies manuales para Logger son especialmente útiles en iOS, donde OSLog no tiene API de prueba.
Verificación del orden de llamadas — algunos escenarios requieren un orden estricto de operaciones: abrir conexión, enviar datos, cerrar conexión. Mockito permite comprobar el orden mediante InOrder.verify(). Un spy hace lo mismo pero preserva la ejecución real. Si tanto el orden como el resultado de cada paso (la conexión realmente se abrió) son importantes — usa un spy, no un mock.
Preguntas frecuentes
Spy envuelve un objeto real y ejecuta su lógica, además de registrar las llamadas. Mock reemplaza completamente el objeto con un stub — no se ejecuta ninguna lógica real. Un spy preserva el comportamiento, un mock no. Elige un spy cuando el funcionamiento real del objeto sea importante; elige un mock cuando necesites aislar la prueba de una dependencia externa.
Cuando un spy provoca operaciones reales de E/S. Si un spy envuelve un objeto que escribe en un archivo, envía HTTP o lee del disco — la prueba deja de ser unitaria. Segundo caso: la prueba solo verifica el valor de retorno sin importar las llamadas — aquí un stub es suficiente y un spy es redundante. Tercero: el código depende del estado interno del spy — esto es una prueba frágil.
Sí, mediante spyk() — el equivalente de Mockito.spy(). MockK.spyk() crea un espía alrededor de un objeto real, soporta every para stubbing parcial y coVerify/coroutinesVerify para funciones suspend. Limitación: no funciona con clases final (necesita open o interface). Para clases Java, MockK también soporta spyk() pero requiere la anotación @MockKJvmInline.
Técnicamente, no. Mock es un stub que no contiene implementación real. Un spy, por definición, envuelve un objeto real. En Mockito, no se puede convertir un mock en un spy. Pero se puede hacer lo contrario: crear un spy y sobrescribir algunos métodos mediante doReturn/when (partial mocking). Esto da un comportamiento similar al de un mock para métodos seleccionados del objeto spy.
Sí, lo requiere. En Swift no hay proxy dinámico como en Java/Kotlin. Para crear un spy se necesita un protocolo que implementen tanto la clase de producción como la clase spy. Un spy basado en protocolos en Swift es una implementación manual que toma un objeto real, le delega llamadas y registra metadatos. Alternativa: la librería Cuckoo, que genera clases spy mediante SourceKit.
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