Testing Cancelling Effect

Hello! Could somebody help me with a problem? Right now I have to cancel one Effect here is my code:

        case .goNext:
            return .cancel(id: WordAudioPlayerCancelId())

This event called when the module did all work and before destroying I need to cancel word audio effect.
Unfortunately, I cannot find a solution to test this code. Nothing mutates State, nothing sends new Actions.

I really don't want to left any floating and untested code in the Reducer. Especially when this code prevents me from the crash) I delete this module after goNext Action was send and I need to cancel any Events in progress.

If you comment out your .cancel effect do you get a failing test?

I would expect that the test fails unless you cancel the effect because then you would have a currently running effect, and the store .assert requires that all effects be completed. So, the mere fact that your test passes means that you have properly torn down that effect.

If that's not the case can you provide some code showing how the effect is started and how you are writing your test?

If you want super fine-grained assertions of precisely when effects are completed, you could use .handleEvents on your mock effect in order to tap into the exact moment it is canceled:

let mockEffect = PassthroughSubject<...>()
var didComplete = false

let store = TestStore( 
  initialState: ...,
  reducer: ...,
  environment: .mock(
    effect: mockEffect
      .handleEvents(receivedCompletion: { _ in  didComplete = true })
      .eraseToEffect()
  )
)

store.assert(
  // [send an action to start the effect]

  .send(.goNext) { 
    ...
    XCTAssertTrue(didComplete)
  }
)

Thank you for the fast answer @mbrandonw. I don't know why I didn't get this way of testing by myself) :upside_down_face:

I've managed to write a proper test, thank you again for the help!

func testAudioCancelation() {
    // Given
    let store = TestStore(
        initialState: ListenAndTranslateState(
            word: WordBuilder().build(),
            options: [],
            selectedOption: nil,
            isAudioPlaying: false
        ),
        reducer: ListenAndTranslateState.reducer,
        environment: environment
    )
    // When
    var didComplete = false
    store.assert(
        .environment {
            $0.wordPlayer = WordAudioPlayer { _ in
                Empty<(), Never>(completeImmediately: false)
                    .map { _ in () }
                    .setFailureType(to: Never.self)
                    .handleEvents(receiveCancel: { didComplete = true })
                    .eraseToEffect()
            }
        },
        .send(.startAudio) {
            $0.isAudioPlaying = true
        },
        .send(.goNext(.null)),
        .do { XCTAssertTrue(didComplete) }
    )
}
1 Like