MockK para los clientes de proveedores
La lección anterior defendía usar infraestructura real en tus pruebas; esta defiende justo lo contrario cuando la infraestructura le pertenece a otra empresa.
Postgres y Mongo son tuyos. Tú controlas el esquema, tú controlas el contenedor, y un error en cómo tu código les habla es un error de tu código. Black Book, Carfax y S&P VIS no son tuyos. Son servicios HTTP de terceros con sus propios límites de tasa, su propia disponibilidad y, algo crítico, una factura real asociada a cada llamada. Correr tu suite de pruebas contra los proveedores reales la haría lenta, inestable (que el entorno de staging de un proveedor se caiga rompe tu build por una razón que nada tiene que ver con tu código) y costosa de una forma que se acumula cada vez que corre el CI. Así que para estos tres clientes, la decisión correcta es la imagen espejo de la lección anterior: simularlos.
MockK es la biblioteca de mocking construida para Kotlin en lugar de adaptada a él, y la razón por la que importa aquí específicamente son las corrutinas. `ValuationProviderClient.fetchValuation` es una `suspend fun`, y una biblioteca de mocking pensada para llamadas de métodos Java planas no tiene vocabulario nativo para eso — MockK sí, a través de `coEvery` y `coVerify`, que son las contrapartes conscientes de corrutinas de la API ordinaria `every`/`verify`.
Levantar un mock es `mockk<ValuationProviderClient>()`, y decirle qué hacer cuando lo llaman es `coEvery { blackBookClient.fetchValuation(any()) } returns someQuote`. `any()` es un matcher de argumentos — dice "sin importar qué VIN se pase, devuelve esto" — y puedes usar `any()` para unas llamadas y un VIN exacto para otras cuando una prueba necesita distinguir entre ambos casos. Por defecto un mock de MockK es estricto: llamar a un método que nunca configuraste hace que la prueba falle de inmediato con un error claro, en lugar de devolver null en silencio y producir un fallo confuso más adelante.
`coVerify` es cómo afirmas que una llamada realmente ocurrió, algo que importa para un servicio que llama a tres proveedores independientes — quieres una prueba que demuestre que `VehicleValuationService` llamó a cada proveedor configurado exactamente una vez por valuación, ni cero veces ni dos por un error de reintento. `coVerify(exactly = 1) { blackBookClient.fetchValuation("1HGCM82633A004352") }` se lee casi como la afirmación que es: esta llamada específica, con este argumento específico, ocurrió exactamente una vez.
El verdadero beneficio de simular estos clientes es lo que te permite hacer con las políticas de reintento y disyuntor de Failsafe que construiste en un módulo anterior: ponerlas bajo el microscopio sin depender de una red real e inestable. `coEvery { carfaxClient.fetchValuation(any()) } throws SocketTimeoutException("Carfax did not respond")` simula de forma determinista exactamente el modo de fallo que Failsafe existe para manejar, en cada ejecución de prueba, sin variación alguna — algo que una llamada de red real jamás podría garantizarte.
Desde ahí puedes llevar el disyuntor por toda su máquina de estados a voluntad. Llama a `valuate()` suficientes veces con ese timeout configurado y puedes afirmar que el disyuntor se abre: después de cierto umbral, `coVerify(atMost = 3) { carfaxClient.fetchValuation(any()) }` demuestra que el servicio dejó de llamar a un cliente que el disyuntor ya dio por perdido, en lugar de seguir golpeando a un proveedor que ya demostró estar caído — que es exactamente el propósito de un disyuntor, ahora demostrable en una prueba que corre en milisegundos.
Puesta junto a la lección anterior, la pauta no es una contradicción — es un mismo principio aplicado a dos situaciones distintas. Prueba lo que es tuyo contra lo real, porque un mock de tu propia base de datos solo demuestra que tus suposiciones eran internamente consistentes, no correctas. Prueba lo que no es tuyo contra un mock, porque lo real es inherentemente poco confiable, y una suite de pruebas que depende de la disponibilidad de otra empresa es una suite que fallará por razones que no tienen nada que ver con si tu código funciona.
class VehicleValuationServiceMockkTest {private val blackBookClient = mockk<ValuationProviderClient>()private val carfaxClient = mockk<ValuationProviderClient>()private val spVisClient = mockk<ValuationProviderClient>()private val cache = mockk<ValuationCacheRepository>(relaxed = true)private val service = VehicleValuationService(providers = listOf(blackBookClient, carfaxClient, spVisClient),cache = cache,rulesEngine = RulesEngine(defaultRuleSet()))@Testfun `blends all three provider quotes into a single valuation`() = runTest {coEvery { blackBookClient.fetchValuation(any()) } returnsProviderQuote(providerName = "BLACK_BOOK", amount = BigDecimal("18250.00"))coEvery { carfaxClient.fetchValuation(any()) } returnsProviderQuote(providerName = "CARFAX", amount = BigDecimal("17980.00"))coEvery { spVisClient.fetchValuation(any()) } returnsProviderQuote(providerName = "SP_VIS", amount = BigDecimal("18100.00"))val result = service.valuate(vin = "1HGCM82633A004352")assertEquals(BigDecimal("18110.00"), result.blendedAmount)}}
Mocking all three provider clients with coEvery and asserting the blended valuation the service computes from their responses.
@Testfun `calls every configured provider exactly once per valuation`() = runTest {coEvery { blackBookClient.fetchValuation(any()) } returns sampleQuote("BLACK_BOOK")coEvery { carfaxClient.fetchValuation(any()) } returns sampleQuote("CARFAX")coEvery { spVisClient.fetchValuation(any()) } returns sampleQuote("SP_VIS")service.valuate(vin = "1HGCM82633A004352")coVerify(exactly = 1) { blackBookClient.fetchValuation("1HGCM82633A004352") }coVerify(exactly = 1) { carfaxClient.fetchValuation("1HGCM82633A004352") }coVerify(exactly = 1) { spVisClient.fetchValuation("1HGCM82633A004352") }}
coVerify confirming each provider was called exactly once for a single valuation request — not zero, not twice.
@Testfun `opens the circuit breaker after repeated Carfax timeouts and stops calling it`() = runTest {coEvery { blackBookClient.fetchValuation(any()) } returns sampleQuote("BLACK_BOOK")coEvery { spVisClient.fetchValuation(any()) } returns sampleQuote("SP_VIS")coEvery { carfaxClient.fetchValuation(any()) } throws SocketTimeoutException("Carfax did not respond")repeat(5) { service.valuate(vin = "1HGCM82633A004352") }coVerify(exactly = 5) { blackBookClient.fetchValuation(any()) }coVerify(atMost = 3) { carfaxClient.fetchValuation(any()) }}
Simulating repeated Carfax timeouts to prove the circuit breaker trips and the service stops calling a provider it has already given up on.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. The previous lesson used real Postgres and Mongo containers instead of mocks. Why is mocking the correct choice for the three provider clients, when it was the wrong choice for the repositories?