MockK for the Provider Clients
The last lesson argued for real infrastructure in your tests — this one argues for the opposite when the infrastructure belongs to someone else.
Postgres and Mongo are yours. You own the schema, you control the container, and a bug in how your code talks to them is a bug in your code. Black Book, Carfax, and S&P VIS are not yours. They are third-party HTTP services with their own rate limits, their own uptime, and — critically — a real invoice attached to every call. Running your test suite against the actual providers would make it slow, flaky (a provider's staging environment going down fails your build for a reason that has nothing to do with your code), and expensive in a way that compounds every time CI runs. So for these three clients, the right call is the mirror image of the last lesson: mock them.
MockK is the mocking library built for Kotlin rather than adapted to it, and the reason it matters here specifically is coroutines. `ValuationProviderClient.fetchValuation` is a `suspend fun`, and a mocking library designed around plain Java method calls has no native vocabulary for that — MockK does, through `coEvery` and `coVerify`, which are coroutine-aware counterparts of the ordinary `every`/`verify` API.
Standing up a mock is `mockk<ValuationProviderClient>()`, and telling it what to do when called is `coEvery { blackBookClient.fetchValuation(any()) } returns someQuote`. `any()` is an argument matcher — it says "regardless of which VIN is passed, return this" — and you can use `any()` for some calls and an exact VIN literal for others when a test needs to distinguish between them. By default a MockK mock is strict: call a method you never stubbed and the test fails immediately with a clear error, rather than silently returning null and producing a confusing downstream failure.
`coVerify` is how you assert that a call actually happened, which matters for a service that calls three independent providers — you want a test proving `VehicleValuationService` called every configured provider exactly once per valuation, not zero times, not twice from a retry bug. `coVerify(exactly = 1) { blackBookClient.fetchValuation("1HGCM82633A004352") }` reads almost like the assertion it is: this specific call, with this specific argument, happened exactly once.
The real payoff of mocking these clients is what it lets you do to the Failsafe retry and circuit-breaker policies you built in an earlier module: put them under a microscope without a real flaky network. `coEvery { carfaxClient.fetchValuation(any()) } throws SocketTimeoutException("Carfax did not respond")` deterministically simulates the exact failure mode Failsafe exists to handle, on every single test run, with zero variance — something an actual network call could never promise you.
From there you can drive the circuit breaker through its full state machine on command. Call `valuate()` enough times with that timeout stubbed in and you can assert the breaker trips: after some threshold, `coVerify(atMost = 3) { carfaxClient.fetchValuation(any()) }` proves the service stopped calling a client the breaker has already given up on, rather than continuing to hammer a provider that has already demonstrated it is down — which is the entire point of a circuit breaker, now provable in a test that runs in milliseconds.
Put side by side with the last lesson, the pattern is not a contradiction — it is a single principle applied to two different situations. Test what you own against the real thing, because a mock of your own database only ever proves your assumptions were internally consistent, not correct. Test what you don't own against a mock, because the real thing is unreliable by nature, and a test suite that depends on someone else's uptime is a test suite that will fail for reasons that have nothing to do with whether your code works.
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.
🧠 Check your understanding
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?