
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.
Useful testing utilities for Compose Multiplatform that aren't part of the official Compose test library:
ComposeUiTest.testHook()
/**
* 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()
/**
* 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)
}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.
To run tests locally:
./gradlew :composetesttools:jvmTest./gradlew :composetesttools:iosSimulatorArm64Test./gradlew :composetesttools:wasmJsBrowserTest./gradlew :composetesttools:connectedAndroidDeviceTestRun a specific test:
./gradlew :library:jvmTest --tests "io.github.notoriouscorgi.composetesttools.TestHookTest"Useful testing utilities for Compose Multiplatform that aren't part of the official Compose test library:
ComposeUiTest.testHook()
/**
* 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()
/**
* 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)
}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.
To run tests locally:
./gradlew :composetesttools:jvmTest./gradlew :composetesttools:iosSimulatorArm64Test./gradlew :composetesttools:wasmJsBrowserTest./gradlew :composetesttools:connectedAndroidDeviceTestRun a specific test:
./gradlew :library:jvmTest --tests "io.github.notoriouscorgi.composetesttools.TestHookTest"