Does anyone know how to test a composable function...
# compose
m
Does anyone know how to test a composable function that that doesn’t actually render UI? For instance, I have an implementation of CheckboxColors which is tied to custom theme attributes. I’d like to test that given a particular theme setup, the right values get returned.
1
The basic steps are: create and start running a Recomposer, create a Composition with an empty/arbitrary Applier since you won't be emitting nodes, setContent, then assert.
If you need to drive time forward to check that things change as expected over time, the example at that link uses a couple utilities written around kotlinx-coroutines-test to do so
m
Thanks @Adam Powell. Any clue where i can find the androidx.compose.runtime.mock.compositionTest function? It’s not in the existing test jars i’m using.
a
Poke around code search at that link and you should be able to find it, it's not published as API, it's just part of compose's internal tests
m
ok.
a
If you'd like to file an official feature request to support something like it as testing API please do, it'll help us measure interest in it. Most of our test library efforts have been around compose UI so far
Though I think some of it will end up gated on the API stability of kotlinx-coroutines-test
m
Ok. It just seems like an aweful lot of cope to have to copy and paste, just by looking a a quick glance.
a
The one to look at is localRecomposerTest and iirc that one's both small and reusable
l
If you are happy to run this as a device test, it’s also really easy to just assert that inside the normal scaffolding for a UI test - that is how a lot of Material tests are built. E.g https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]roidTest/kotlin/androidx/compose/material/ContentAlphaTest.kt
a
You can, but it's kinda overkill
l
Yep, but probably easier until we have out of the box support for non-device tests, right now there’s a chunk of boilerplate you need
m
Yeah. I can probably copy some of that and build a wrapper around the test procedure so that you can just pass it a lambda function.
a
The "chunk of boilerplate" is less than a hundred lines of code, one time dumped into a TestUtils.kt file, that then produces incredibly short and readable tests involving no test rules that run ~instantly without connecting a device or emulator. I struggle to think of a better deal in compose testing. 😛
m
Yeah, I’ve got it working now. One more quick question. Is there a way to test actual UI compositions in this isolated way as well? Ie. composing something and then checking semantic properties, etc… like you’d do in an actual ui test?
basically the kind of stuff you’d do with composeTestRule
a
not as easily as we'd like. The team has been generally excited about the idea of using the compose desktop code paths for doing it but we've got some dots to connect
m
ok. thanks.
Implementing our internal design system has been quite interesting. It’s actually helped me learn a lot more about the ins and outs of compose than i expected. Just the theming alone.
Also @Adam Powell The whole idea of the SemanticProperties is going to take some getting used to for testing. Figuring out when it’s actually worth adding the overhead to the tree for testing purposes.
a
yeah with the kind of state vs. composable function decoupling you can do with compose, it sort of changes the options for effective testing strategies. Checking semantics for making sure your composable functions assembled the right things is helpful, but I've found myself writing much fewer tests of that variety and a lot more unit tests of hoisted state objects where the bulk of logic ends up congregating
I find that I don't often need to use custom semantic annotations for testing nearly as often and usually lean more on looking for text or other existing structure
d
unrelated note: You might want to
remember
that
derivedStateOf
.
a
there are a few things about that specific code that probably aren't working as intended
derivedStateOf
isn't delivering any value here, it might as well just `return fill`; a composable function that returns a value will still cause its caller to recompose and it's using its own recomposition to change values it returns, not an independently running effect like
.collectAsState
or similar
once you
remember
it as per @Dominaezzz’s comment you'll realize that you need the actual value to change, and remembering it as-is will mean it just closes over the first calculated value. We don't have a
CompositionLocal<T>.currentAsState(): State<T>
that would make putting the actual calculation inside the
derivedStateOf {}
block do what you want, but we should probably add it at some point