I'm trying to test cancelling an effect that loads media and sends updates back to the store. Simplified it looks like this:
case .binding(\.mediaLoader.$results):
let results = state.mediaLoader.results
state.creationState = .importingMedia(Progress(max: results.count))
return .run { send in
let media = results.enumerated().asyncMap({ (index, result) in
// Do a lot of stuff
await send(.updateProgress(index + 1))
})
await send(.save(media))
}
.cancellable(id: MediaCreation.self)
I cancel the effect with:
case .cancelCreation:
state.creationState = .none
return .cancel(id: MediaCreation.self)
The problem is that there is no way to control the asynchronous context the effect runs in, and so you may think you are cancelling the work at the soonest possible moment, but in reality the effect is getting a little bit of time to do its thing. Stephen and I started a very long thread on this very topic in order to understand the best way to test async code. That may help explain how complex of a situation this is, and how hopefully in the future Swift can provide some more tools.
With that said, there are ways to make your test assert what you want. The first way, and probably the best, has to do with what you have marked as // Do a lot of stuff. In that section of the effect are you reaching out to the environment to perform some asynchronous work? If so, then you can make a mock of that dependency to perform a sleep so that you can simulate it taking a long time, making it possible for the cancellation to happen before any work is processed.
If that is not possible, then the other option that comes to mind is to use a scheduler to sleep in the effect so that you can perform cancellation:
return .run { send in
self.environment.mainQueue.sleep(for: .seconds(0))
// ...
}
Then in tests you can use a TestScheduler, which will effectively hold up that effect forever so that you can cancel it. And in all other tests you can use an ImmeduateScheduler.
This second style is more hacky since you are inserting code into production code just to make things testable. The first style is more preferable as it emulates what would actually happen in real life.