Testing¶
Presenters are Composables that return values, so they can be tested as functions — no emulator, no Robolectric, no Compose UI test rule.
The harness runs on every target, so these tests execute on the JVM and on iOS.
Testing a presenter¶
Presenter.test { } runs the presenter and gives you a Turbine over its emitted states.
@Test
fun incrementingBumpsTheCount() = runTest {
CounterPresenter(FakeNavigator(CounterScreen)).test {
val initial = awaitItem()
assertEquals(0, initial.count)
initial.eventSink(CounterEvent.Increment)
assertEquals(1, awaitItem().count)
cancelAndIgnoreRemainingEvents()
}
}
The rhythm is always the same: await a state, send an event through its eventSink, await the
next state. Finish with cancelAndIgnoreRemainingEvents() so the presenter is torn down.
test accepts a timeout if a presenter does slow work:
FakeNavigator¶
Records navigation without a real back stack:
@Test
fun openingAnItemNavigates() = runTest {
val navigator = FakeNavigator(HomeScreen)
HomePresenter(navigator).test {
awaitItem().eventSink(HomeEvent.OpenDetail("42"))
assertEquals(DetailScreen("42"), navigator.awaitNextScreen())
cancelAndIgnoreRemainingEvents()
}
}
| Method | Asserts on |
|---|---|
awaitNextScreen() |
the next goTo |
awaitPop() |
the next pop, with its PopResult |
awaitResetRoot() |
the next resetRoot, with its flags |
assertIsEmpty() |
the back stack is empty |
expectNoEvents() |
nothing navigated |
Seed it with a starting stack, root first:
Pop results come back on the event:
Asserting nothing happened¶
@Test
fun incrementDoesNotNavigate() = runTest {
val navigator = FakeNavigator(CounterScreen)
CounterPresenter(navigator).test {
awaitItem().eventSink(CounterEvent.Increment)
awaitItem()
cancelAndIgnoreRemainingEvents()
}
navigator.expectNoEvents()
}
Overlays¶
FakeOverlayHost intercepts what a presenter shows and answers it:
val overlayHost = FakeOverlayHost()
val shown = overlayHost.awaitOverlay()
assertTrue(shown is ConfirmOverlay)
overlayHost.finish(true)
Provide it via LocalOverlayHost in the composition under test.
Retained state in tests¶
With no registry provided, rememberRetained falls back to NoOpRetainedStateRegistry and
behaves like remember. Presenter tests therefore need no setup, and each test { } starts
clean.
To exercise retention itself, provide a registry and re-run the presenter against it:
Testing UIs¶
UIs are ordinary Composables taking a state object, so they need no Orbit-specific tooling — construct a state and use the standard Compose UI test APIs:
composeTestRule.setContent {
HomeContent(HomeState(items = persistentListOf("Item 1")) { })
}
composeTestRule.onNodeWithText("Item 1").assertIsDisplayed()
Because the UI only reads its state parameter, every case — loading, error, empty — is reachable by passing a different state. There is no need to drive a presenter into that condition first.
What still needs a device¶
Some behaviour is integration-level and unit tests cannot reach it:
- retained state surviving a real configuration change,
- the back stack rebuilding after process death,
- transitions and shared elements.
Verify those on an emulator or simulator when you touch navigation or retention.