How to test concatenated effects which call other effects?

I have a problem testing coupled concatenated action/effects. To demonstrate the problem I modified the
02-Effects-Basics and 02-Effects-BasicsTests code. I added 3 rather simple actions

enum EffectsBasicsAction: Equatable {
  case multiAction(Int)
  case multiplyCountBy(Int)
  case decrementCountBy(Int)
...

decrementCountBy is queued to multiplyCountBy and is itself concatenated multiple times in multiAction.

...
  case .multiAction(var repCount):
    var effects: [Effect<EffectsBasicsAction, Never>] = []
    while repCount > 0 {
        effects.append(Effect(value: .multiplyCountBy(state.count)))
        repCount -= 1
    }
    return Effect.concatenate(effects).delay(for: 1, scheduler: environment.mainQueue).eraseToEffect()

  case .multiplyCountBy(let factor):
    state.count *= factor
    return Effect(value: .decrementCountBy(5))

  case .decrementCountBy(let number):
    state.count -= number
    return .none
...
I inserted another button into EffectsBasicsView to start multiAction and modified the starter count state.
struct EffectsBasicsState: Equatable {
  var count = -3
  ...

struct EffectsBasicsView: View {
...
            Button("Multiple actions") { viewStore.send(.multiAction(3)) }
...

To test this I created a new test.

func testMultiActionDown() {
  let store = TestStore(
    initialState: EffectsBasicsState(),
    reducer: effectsBasicsReducer,
    environment: EffectsBasicsEnvironment(
      mainQueue: self.scheduler.eraseToAnyScheduler(),
      numberFact: { _ in fatalError("Unimplemented") }
    )
  )
  store.assert(
    .send(.multiAction(3)) {
      $0.count = -3
    },
    .do { self.scheduler.advance(by: 1) },
    .receive(.multiplyCountBy(-3)) {
        $0.count = 9
    },
    .receive(.multiplyCountBy(-3)) {
        $0.count = -27
    },
    .receive(.multiplyCountBy(-3)) {
        $0.count = 81
    },
    .receive(.decrementCountBy(5)) {
        $0.count = 76
    },
    .receive(.decrementCountBy(5)) {
        $0.count = 71
    },
    .receive(.decrementCountBy(5)) {
        $0.count = 66
    }
  )
}

The order of actions in the app is multiAction->multiplyCountBy->decrementCountBy (the last two repeated).
For the test to succeed the actions have to be ordered as shown, which succeeds but results in a different state.count. The flow inside the app is correct.

Is it possible to handle such situations in TCA?

Looks like you've stumbled on a subtle bug in the test store's implementation. I've filed an issue here to track: Test stores can process actions from effects in the wrong order · Issue #277 · pointfreeco/swift-composable-architecture · GitHub

The fix is unfortunately not as straightforward as we hoped, but maybe something will come to us soon.

@baldvader We think we've managed a fix here: Drive Test Store with a real Store by stephencelis · Pull Request #278 · pointfreeco/swift-composable-architecture · GitHub

Can you run your tests against the main branch and let us know how things go?

@stephencelis: Yes, that solved my problem (running the main branch on Xcode 12 beta 6)!

Thank you for your help and work creating this library!

1 Like

Great! We just cut a release for 0.8.0, so you shouldn't need to pin to the main branch anymore.