How to test receiving merged effects

I have the following reducer case:

case .cancelButtonTapped:
    return .merge(
        .cancel(id: Creation.self),
        .task {
            .textCreation(.cancelCreation)
        },
        .task {
            .mediaCreation(.cancelCreation)
        }
    )

How do I test this?

My current test looks as follows:

func testCancelButtonTapped() async throws {
    let store = TestStore(
        initialState: .init(),
        reducer: CreationFeature()
    )
    
    _ = await store.send(.cancelButtonTapped)
    await store.receive(.textCreation(.cancelCreation))
    await store.receive(.mediaCreation(.cancelCreation))
}

The test randomly passes and fails depending on the order in which the actions are received.

Is there any way to test this in a reliable way?

1 Like

Hi @dominik.mayer, it appears that you are sending synchronous actions from effects as a means to share logic in your reducer, and I would like to point out that we highly recommend against that. The linked article also demonstrates how this style of sharing logic also muddies tests.

I recommend trying to refactor this code to share logic via simpler mechanisms, such as just calling out to helper functions or even just performing all the work directly inside an Effect.run when handling .cancelButtonTapped. Once you have done that I will be curious to see if you still have the ordering problems (it's definitely possible), and if so there are a few things you can try to fix it.

Thanks for sharing, @mbrandonw. That makes a lot of sense.

I think my problem is inter-reducer communication. I have one parent with a cancel button that should cancel an effect running in a child reducer and resetting the state of two child reducers.

I think I can cancel the effect from the parent by also returning .cancel(id: MediaCreationFeature.MediaCreation.self).

More difficult are the state changes. Let’s take the former .textCreation(.cancelCreation) as an example. I’ve refactored it according to your suggestion into a resetCreationState(_ state: inout State) function. I can call this function from all the points in the child reducer that used to return the retired action. The tests look much cleaner.

But this method is not easily accessible from the parent reducer. I would have to move it to the global namespace or make it static and call it with TextCreationFeature.resetCreationState(&state.textCreation). Is that how you would do it?

I assume that if it was the opposite way – the child asking the parent to do some work – I would still have to use actions as using methods would couple the child to the parent.