Hi guys,
I have a situation where we have a TopLevelFeature
, which Scopes multiple standalone Child features (FeatureA, FeatureB, FeatureC etc). When sending a .refresh
Action to the TopLevelFeature
, this calls the reducer for each standalone feature and maps each feature's reload Action like so:
private func whenRefreshing(_ state: inout State) -> Effect<Action> {
let featureA = FeatureA().reduce(into: &state.featureA, action: .reload).map(Action.featureA)
let featureB = FeatureB().reduce(into: &state.featureB, action: .reload).map(Action.featureB)
let featureC = FeatureC().reduce(into: &state.featureC, action: .reload).map(Action.featureC)
let featureD = FeatureD().reduce(into: &state.featureD, action: .reload).map(Action.featureD)
let featureE = FeatureE().reduce(into: &state.featureE, action: .reload).map(Action.featureE)
return Effect.merge([
featureA,
featureB,
featureC,
featureD,
featureE
])
}
Each Child Feature looks like this
@Reducer
struct FeatureA {
@ObservableState
struct State: Equatable {
}
enum Action {
case reload
case response
}
var body: some ReducerOf<Self> {
Reduce<State, Action> { state, action in
switch action {
case .reload:
return someWork()
case .response:
return .none
}
}
}
private func someWork() -> Effect<Action> {
.run { send in
await send(.response)
}
}
}
The code seems to run fine (i.e. Effects run in parallel), but when running tests it has some behavior I was not expecting:
func test() async throws {
let store = TestStore(
initialState: TopLevelFeature.State()) {
TopLevelFeature()
}
store.exhaustivity = .off
await store.send(.refresh)
await store.receive(\.featureA.response)
await store.receive(\.featureB.response)
await store.receive(\.featureC.response)
await store.receive(\.featureD.response)
await store.receive(\.featureE.response)
}
Only featureA and featureB pass, the others get a Expected to receive an action matching case path, but didn't get one.
test failure.
Changing the order in which they're awaited, or the order in which the Effects are merged in the production code, changes the results of the test.
The are only two instances in which all tests pass:
- In the test actions are awaited in exactly reverse order in which they're merged in the production code.
- The production code uses a series of nested
merge(with: )
(which only merges two Effects into one) and test awaits for the actions in the exact same order (as shown below):
private func whenRefreshing(_ state: inout State) -> Effect<Action> {
let featureA = FeatureA().reduce(into: &state.featureA, action: .reload).map(Action.featureA)
let featureB = FeatureB().reduce(into: &state.featureB, action: .reload).map(Action.featureB)
let featureC = FeatureC().reduce(into: &state.featureC, action: .reload).map(Action.featureC)
let featureD = FeatureD().reduce(into: &state.featureD, action: .reload).map(Action.featureD)
let featureE = FeatureE().reduce(into: &state.featureE, action: .reload).map(Action.featureE)
return featureA
.merge(with: featureB
.merge(with: featureC
.merge(with: featureD
.merge(with: featureE ))))
}
I'm wondering whether there's a mechanism in the TestStore that guarantees that they are awaited in the same order, perhaps to avoid flaky results? But why would it be in reverse order (see option 1 above), or same order but with nested merge(with: )
?
There's prob an obvious answer to this but it's eluding me.
What is the way to write a valid test for this type of use case?
Thanks a lot!
Danny