Skip to main content

The Promise

ViewModel Weight-Loss Program™

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

BeforeAfter
ViewModel constructor params10+1
Mocking library requiredYesNo
Test setup lines per test20 – 403 – 8
Coverage target achievableHardStraightforward

Next, see how this maps onto Vertical Slice Architecture →