The Promise
Put your oversized ViewModel on a diet — results in one week. Before: 10+ constructor parameters, 40 lines of mock setup, a test suite that dreads every refactor. After: 1 dependency, 5-line tests, zero mocking libraries. Not a real program. Typical results achieved in the first PR.
The Problem: an XXL ViewModel
Real-world ViewModels tend to grow. Each new feature pulls in another dependency until the constructor looks like this:
class InitialViewModel(
private val applicationMetadata: ApplicationMetadata,
private val retrieveAndStoreTogglesUseCase: RetrieveAndStoreTogglesUseCase,
watchTogglesUseCase: WatchTogglesUseCase,
private val persistCachedInfoUseCase: PersistCachedInfoUseCase,
private val fetchActiveUserAndStoreUseCase: FetchActiveUserAndStoreUseCase,
fetchPreferredLocaleUseCase: FetchPreferredLocaleUseCase,
fetchVisualThemeUseCase: FetchVisualThemeUseCase,
private val metricsReporterPort: MetricsReporterPort,
val runtimeSettings: RuntimeSettings,
val speedMonitor: SpeedMonitor,
val cloudPerformanceTracker: PerformanceTraceListener,
val simpleLoggingTracker: SimpleLoggingTracker,
) : ViewModel()
10+ dependencies. Testing this requires constructing or mocking all 10+, even for a test that only cares about one use-case. The mock setup often dwarfs the actual test logic.
The Solution: an XXS ViewModel
With MediatorK the ViewModel has exactly one dependency:
class OrderViewModel(private val mediator: Mediator) : ViewModel() {
fun createOrder(id: String, amount: Double) {
viewModelScope.launch {
_state.update { it.copy(isLoading = true, error = null) }
try {
val result = mediator.send(CreateOrderCommand(id = id, amount = amount))
_state.update { it.copy(orderResult = result, isLoading = false) }
} catch (e: Exception) {
_state.update { it.copy(error = e.message, isLoading = false) }
}
}
}
}
Every action becomes a mediator.send(...) call. The ViewModel no longer knows which use-case, repository, or data source handles the request; it just dispatches.
The Testing Story
Reducing the ViewModel to one dependency changes how you test it. Instead of constructing or mocking all 10+ real dependencies, you swap in a single FakeMediator and register whatever handler the test needs:
// Before — mock 10+ dependencies just to test one scenario
@Test
fun `place order - notifies user on success`() {
val notificationService = mockk<NotificationService>()
val inventoryRepo = mockk<InventoryRepository>()
val orderRepo = mockk<OrderRepository>()
val paymentGateway = mockk<PaymentGateway>()
// … 8 more mocks, 10+ stubs …
val vm = OrderViewModel(notificationService, inventoryRepo, orderRepo, paymentGateway, …)
vm.placeOrder(cart)
verify { notificationService.notify(match { it.type == "ORDER_PLACED" }) }
}
// After — one fake, no mocking library
@Test
fun `createOrder success updates state with result`() = runTest {
val fakeMediator = FakeMediator()
fakeMediator.register(
fakeHandler<CreateOrderCommand, OrderResult> { _, _, _ ->
OrderResult(orderId = "ORD-1", responseTime = 10)
}
)
val vm = OrderViewModel(fakeMediator)
vm.createOrder("1", 99.0)
advanceUntilIdle()
assertEquals("ORD-1", vm.stateFlow.value.orderResult?.orderId)
assertFalse(vm.stateFlow.value.isLoading)
}
The ViewModel test does not import a mocking library. It does not know about repositories or services. It tests exactly one thing: how the ViewModel reacts to a mediator response.
Summary
| Before | After | |
|---|---|---|
| ViewModel constructor params | 10+ | 1 |
| Mocking library required | Yes | No |
| Test setup lines per test | 20 – 40 | 3 – 8 |
| Coverage target achievable | Hard | Straightforward |
Next, see how this maps onto Vertical Slice Architecture →