ComposeTestTools

Testing utilities for Compose hooks and composables: testHook, renderHook and waitForCondition enabling assertion of non-UI hook values, composition-local wrappers, clock control and fast unit-hook rendering.

Android JVMJVMKotlin/NativeWasm
GitHub stars2
Open issues0
LicenseApache License 2.0
Creation dateabout 1 month ago

Last activity29 days ago
Latest releasev0.1.0 (29 days ago)

Compose Test Tools

What are they?

Useful testing utilities for Compose Multiplatform that aren't part of the official Compose test library:

ComposeUiTest.testHook()

  • Use for testing a composable hook that produces values that ARE NOT ui components (think data fetching)
/**
 * Assert a simple value
 */
@Test
fun testHookCapturesRememberedState() = runComposeUiTest {
    val result by testHook {
        remember { mutableStateOf(42) }
    }
    assertEquals(42, result)
}

/**
 * Assert a value that requires a wrapper to set it up
 */
@Test
fun testHookWithCompositionLocalWrapper() {
    val TestLocal = compositionLocalOf { "default" }

    runComposeUiTest {
        val result by testHook(
            wrapper = { content ->
                CompositionLocalProvider(TestLocal provides "injected") {
                    content()
                }
            },
            hook = { TestLocal.current }
        )
        assertEquals("injected", result)
    }
}

/**
 * Assert a value when the value changes
 */
@Test
fun testHookWithLateChangingStateAndClockAdvancement() {
    runComposeUiTest {
        mainClock.autoAdvance = false
        val changingValue by testHook {
            rememberLateChangingVal()
        }
        println(changingValue)
        assertEquals(changingValue?.first, false)

        // Advance time
        mainClock.advanceTimeBy(changingValue?.second!!)
        assertEquals(changingValue?.first, true)
    }
}

ComposeUiTest.waitForCondition()

  • When you cant advanceTimeUntil(), wait for your assertion
/**
 * Wait for a condition to be asserted
 */
@Test
fun testWaitForCondition() {
    runComposeUiTest {
        val changingValue by testHook {
            rememberLateChangingVal()
        }
        assertEquals(changingValue?.first, false)

        // waitUntil advances the clock with each iteration, so we can "lose"
        // time as it goes through each iteration as it skips ahead faster than via
        // manual advancement. Checking 1 ms is safe unless the machine is VERY fast
        assertFailsWith<ComposeTimeoutException> {
            waitForCondition(timeoutMs = 1L) {
                assertEquals(changingValue?.first, true)
            }
        }

        // The value changed by this point
        waitForCondition(timeoutMs = changingValue?.second!!) {
            assertEquals(changingValue?.first, true)
        }

    }
}

TestScope.renderHook() Useful for running tests to assert your composable hooks that return data, but in a faster unit test environment. This is useful so you don't need to have a full instrumented test, which is slower. Almost entirely taken from https://github.com/cashapp/molecule, however useful if you don't want to use the molecule framework for decoupling business and ui logic

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun reactsToStateChanges() = runTest {
        val trigger = CompletableDeferred<Unit>()
        val state by renderHook {
            rememberLateChangingVal(deferTrigger = trigger)
        }
        assertEquals(false, state)

        act { trigger.complete(Unit) }

        assertEquals(true, state)
    }

Installation

Add the dependency to your build.gradle.kts:

// Kotlin Multiplatform (all platforms), or iOS, WASMJs
kotlin {
    sourceSets {
        commonTest.dependencies {
            implementation("io.github.notoriouscorgi:composetesttools:1.0.0")
        }
    }
}
// Android-only
dependencies {
    androidTestImplementation("io.github.notoriouscorgi:composetesttools-android:1.0.0")
}
// JVM-only
dependencies {
    testImplementation("io.github.notoriouscorgi:composetesttools-jvm:1.0.0")
}

The library is published to Maven Central — no additional repository configuration needed.

Dev

To run tests locally:

./gradlew :composetesttools:jvmTest
./gradlew :composetesttools:iosSimulatorArm64Test
./gradlew :composetesttools:wasmJsBrowserTest
./gradlew :composetesttools:connectedAndroidDeviceTest

Run a specific test:

./gradlew :library:jvmTest --tests "io.github.notoriouscorgi.composetesttools.TestHookTest"
Android JVMJVMKotlin/NativeWasm
GitHub stars2
Open issues0
LicenseApache License 2.0
Creation dateabout 1 month ago

Last activity29 days ago
Latest releasev0.1.0 (29 days ago)

Compose Test Tools

What are they?

Useful testing utilities for Compose Multiplatform that aren't part of the official Compose test library:

ComposeUiTest.testHook()

  • Use for testing a composable hook that produces values that ARE NOT ui components (think data fetching)
/**
 * Assert a simple value
 */
@Test
fun testHookCapturesRememberedState() = runComposeUiTest {
    val result by testHook {
        remember { mutableStateOf(42) }
    }
    assertEquals(42, result)
}

/**
 * Assert a value that requires a wrapper to set it up
 */
@Test
fun testHookWithCompositionLocalWrapper() {
    val TestLocal = compositionLocalOf { "default" }

    runComposeUiTest {
        val result by testHook(
            wrapper = { content ->
                CompositionLocalProvider(TestLocal provides "injected") {
                    content()
                }
            },
            hook = { TestLocal.current }
        )
        assertEquals("injected", result)
    }
}

/**
 * Assert a value when the value changes
 */
@Test
fun testHookWithLateChangingStateAndClockAdvancement() {
    runComposeUiTest {
        mainClock.autoAdvance = false
        val changingValue by testHook {
            rememberLateChangingVal()
        }
        println(changingValue)
        assertEquals(changingValue?.first, false)

        // Advance time
        mainClock.advanceTimeBy(changingValue?.second!!)
        assertEquals(changingValue?.first, true)
    }
}

ComposeUiTest.waitForCondition()

  • When you cant advanceTimeUntil(), wait for your assertion
/**
 * Wait for a condition to be asserted
 */
@Test
fun testWaitForCondition() {
    runComposeUiTest {
        val changingValue by testHook {
            rememberLateChangingVal()
        }
        assertEquals(changingValue?.first, false)

        // waitUntil advances the clock with each iteration, so we can "lose"
        // time as it goes through each iteration as it skips ahead faster than via
        // manual advancement. Checking 1 ms is safe unless the machine is VERY fast
        assertFailsWith<ComposeTimeoutException> {
            waitForCondition(timeoutMs = 1L) {
                assertEquals(changingValue?.first, true)
            }
        }

        // The value changed by this point
        waitForCondition(timeoutMs = changingValue?.second!!) {
            assertEquals(changingValue?.first, true)
        }

    }
}

TestScope.renderHook() Useful for running tests to assert your composable hooks that return data, but in a faster unit test environment. This is useful so you don't need to have a full instrumented test, which is slower. Almost entirely taken from https://github.com/cashapp/molecule, however useful if you don't want to use the molecule framework for decoupling business and ui logic

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun reactsToStateChanges() = runTest {
        val trigger = CompletableDeferred<Unit>()
        val state by renderHook {
            rememberLateChangingVal(deferTrigger = trigger)
        }
        assertEquals(false, state)

        act { trigger.complete(Unit) }

        assertEquals(true, state)
    }

Installation

Add the dependency to your build.gradle.kts:

// Kotlin Multiplatform (all platforms), or iOS, WASMJs
kotlin {
    sourceSets {
        commonTest.dependencies {
            implementation("io.github.notoriouscorgi:composetesttools:1.0.0")
        }
    }
}
// Android-only
dependencies {
    androidTestImplementation("io.github.notoriouscorgi:composetesttools-android:1.0.0")
}
// JVM-only
dependencies {
    testImplementation("io.github.notoriouscorgi:composetesttools-jvm:1.0.0")
}

The library is published to Maven Central — no additional repository configuration needed.

Dev

To run tests locally:

./gradlew :composetesttools:jvmTest
./gradlew :composetesttools:iosSimulatorArm64Test
./gradlew :composetesttools:wasmJsBrowserTest
./gradlew :composetesttools:connectedAndroidDeviceTest

Run a specific test:

./gradlew :library:jvmTest --tests "io.github.notoriouscorgi.composetesttools.TestHookTest"