Skip to content

Component Testing

The decompose-test module provides infrastructure for unit-testing Decompose components in Kotlin Multiplatform projects.

Dependency

// commonMain test source set (or platform-specific test source set)
testImplementation("app.futured.arkitekt:decompose-test:6.0.0-beta05")

For KMP modules, add it to the test source set in your build.gradle.kts:

kotlin {
    sourceSets {
        commonTest {
            dependencies {
                implementation("app.futured.arkitekt:decompose-test:6.0.0-beta05")
            }
        }
    }
}

ComponentTest

ComponentTest is an interface that wires up the testing environment for a BaseComponent:

Member Type Description
testScope TestScope Shared coroutine scope for the test body and runComponentTest
testDispatcher TestDispatcher Installed as Dispatchers.Main during each test
lifecycleRegistry LifecycleRegistry Drives the component lifecycle — call lifecycleRegistry.create() to start it
componentContext DefaultComponentContext Pass this to the component under test
setup() @BeforeTest Installs testDispatcher as Dispatchers.Main
cleanup() @AfterTest Destroys the lifecycle (if alive) and resets Dispatchers.Main

ComponentTestPreparation

ComponentTestPreparation is the default implementation. Use Kotlin class delegation to wire up your test class:

class LoginComponentTest : ComponentTest by ComponentTestPreparation() {

    @Test
    fun `state updates after login`() = runComponentTest {
        lifecycleRegistry.create()
        val component = LoginComponent(
            componentContext = componentContext,
            navigation = FakeLoginNavigation(),
            loginUseCase = FakeLoginUseCase(),
        )
        component.logIn()
        runCurrent()

        assertEquals("John Doe", component.state.value.fullName)
    }
}

ComponentTestPreparation is open — subclass it to add project-specific fixtures or helpers shared across multiple test classes.

runComponentTest

runComponentTest is a helper that runs your test body inside testScope:

fun runComponentTest(
    timeout: Duration = 60.seconds,
    testBody: suspend TestScope.() -> Unit,
)

It delegates to testScope.runTest(timeout, testBody). Use it as the test body launcher instead of runTest directly so the scope and dispatcher match the component under test.

Driving the Lifecycle

The component lifecycle is advanced by calling Essenty lifecycle extensions on lifecycleRegistry:

lifecycleRegistry.create()   // INITIALIZED → CREATED
lifecycleRegistry.start()    // CREATED → STARTED
lifecycleRegistry.resume()   // STARTED → RESUMED
lifecycleRegistry.stop()     // RESUMED → STARTED
lifecycleRegistry.destroy()  // → DESTROYED (cancels lifecycleScope, closes events channel)

Note

cleanup() automatically destroys the lifecycle after each test if it hasn't been destroyed yet.

Testing State

Assert on componentState (exposed as a StateFlow) to verify state changes:

@Test
fun `initial state`() = runComponentTest {
    lifecycleRegistry.create()
    val component = createComponent()

    assertEquals(LoginState(), component.state.value)
}

Tip

Turbine provides a nicer API for testing Flow and StateFlow emissions, including componentState updates.

Testing Events

Collect events inside backgroundScope to capture one-shot events:

@Test
fun `error event is emitted on failure`() = runComponentTest {
    lifecycleRegistry.create()
    val component = createComponent()

    val received = mutableListOf<LoginUiEvent>()
    backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
        component.events.collect { received.add(it) }
    }

    component.logIn() // triggers an error
    runCurrent()

    assertEquals(listOf(LoginUiEvent.ShowError("Auth failed")), received)
}

Testing Lifecycle Teardown

Verify that the component cleans up correctly on destroy:

@Test
fun `events flow completes on destroy`() = runComponentTest {
    val scope = CoroutineScope(testDispatcher + Job())
    val component = LoginComponent(componentContext, scope, FakeLoginNavigation(), FakeLoginUseCase())
    lifecycleRegistry.create()

    var completed = false
    backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
        component.events.onCompletion { completed = true }.collect { }
    }

    lifecycleRegistry.destroy()

    assertTrue(completed)
}