I'm using molecule for tests and finding that I so...
# squarelibraries
e
I'm using molecule for tests and finding that I sometimes have to use
delay(1)
when causing state changes within a
LaunchedEffect
so that molecule doesn't skip any state changes:
Copy code
runTest {
      moleculeFlow(RecompositionMode.Immediate) {
        val state = model.currentState()

        LaunchedEffect(Unit) {
          model.validate("")
          delay(1)
          state.text.edit { append("A") }
        }

        state
      }.test {
        awaitItem().validationResult shouldBe NameValidator.Result.Valid
        awaitItem().validationResult shouldBe NameValidator.Result.Required // without delay(1) this is missed
        awaitItem().validationResult shouldBe NameValidator.Result.Valid
        cancel()
      }
    }
My setup doesn't seem to be the most typical to begin with, but is there something I could change to not need the
delay(1)
?
s
e
Not as far as I know.
model.currentState()
is backed by a Compose
MutableState
, and I don't think they do anything like that.
s
right, but isn't your test function testing a state flow?
whoops i see you're using
moleculeFlow
which returns a
Flow
and not a
StateFlow
👆 1
k
Just because you're using
moleculeFlow
doesn't mean that the compose state isn't switching values so quickly that it doesn't have time to recompose for an intermediary state, and thus produce a new emission
The
delay
here allows other coroutines to run, which in this case is a recomposition that then emits your value
This seems like a very contrived example of your actual test you're writing. If it is, know that you should be coordinating your test dependencies and your system under test. For example, if the
delay
is standing in for a network call, it would be better to not allow that function to return until your SUT is ready. We do this by queuing elements into our fake network service only after the test is otherwise ready for the network call to complete
e
That is the exact test 😅
Using
yield
doesn't work though, which I found weird.
The
model
basically looks like
Copy code
class Model(val validator: NameValidator) {
  private var validationResult = mutableStateOf(NameValidator.Result.Valid)

  @Composable
  fun currentState() = MyState(
    text = rememberTextFieldState(),
    validationResult = validationResult,
  )

  fun validate(text: CharSequence): Boolean {
    validationResult = validator.validate(text)
    return validationResult.isValid
  }
}
k
When you say "that is exactly the test" do you mean the sample above? Or that you're faking a network request
e
The code I posted above is the actual test in my codebase
Maybe it's something internal in
TextFieldState
?
k
I doubt it. Without the delay you're synchronously editing the state's value twice. If I were you I'd model the test like this:
Copy code
runTest {
      val trigger = Channel<Unit>()
      moleculeFlow(RecompositionMode.Immediate) {
        val state = model.currentState()

        LaunchedEffect(Unit) {
          model.validate("")
          trigger.recieve()
          state.text.edit { append("A") }
        }

        state
      }.test {
        awaitItem().validationResult shouldBe NameValidator.Result.Valid
        trigger.send(Unit)
        awaitItem().validationResult shouldBe NameValidator.Result.Required 
        awaitItem().validationResult shouldBe NameValidator.Result.Valid
        cancel()
      }
    }
This channel coordinates your SUT and your testing code.
e
Ok I thought with Immediate it would still pick up all the state changes even synchronously.
k
Nope. Frame clocks in compose are still implemented with suspending function, so they still need to be dispatched. The immediate frame clock will make its best effort to recompose as soon as a state's value is changed, but there's no guarantee it catches every state change.
e
OK so for tests like these coordination is the best way to make sure that all the changes are picked up
k
Yes.
Typically, I have a fake of some sort that suspends until you enqueue an element in it. Eg.
Copy code
class MyFakeNetworkService : NetworkService {
  
  private val channel = Channel<Foo>(Channel.Buffered)

  suspend fun getFoo(): Foo = channel.recieve()

  fun enqueueFoo(foo: Foo) = channel.trySend(foo).getOrThrow()
}