|
222 | 222 | line: UInt = #line |
223 | 223 | ) { |
224 | 224 | var receivedActions: [(action: Action, state: State)] = [] |
225 | | - var longLivingEffects: [String: Set<UUID>] = [:] |
| 225 | + var longLivingEffects: Set<LongLivingEffect> = [] |
226 | 226 | var snapshotState = self.state |
227 | 227 |
|
228 | 228 | let store = Store( |
229 | 229 | initialState: self.state, |
230 | 230 | reducer: Reducer<State, TestAction, Void> { state, action, _ in |
231 | 231 | let effects: Effect<Action, Never> |
232 | | - switch action { |
| 232 | + switch action.origin { |
233 | 233 | case let .send(localAction): |
234 | 234 | effects = self.reducer.run(&state, self.fromLocalAction(localAction), self.environment) |
235 | 235 | snapshotState = state |
|
239 | 239 | receivedActions.append((action, state)) |
240 | 240 | } |
241 | 241 |
|
242 | | - let key = debugCaseOutput(action) |
243 | | - let id = UUID() |
| 242 | + let effect = LongLivingEffect(file: action.file, line: action.line) |
244 | 243 | return |
245 | 244 | effects |
246 | 245 | .handleEvents( |
247 | | - receiveSubscription: { _ in longLivingEffects[key, default: []].insert(id) }, |
248 | | - receiveCompletion: { _ in longLivingEffects[key]?.remove(id) }, |
249 | | - receiveCancel: { longLivingEffects[key]?.remove(id) } |
| 246 | + receiveSubscription: { _ in longLivingEffects.insert(effect) }, |
| 247 | + receiveCompletion: { _ in longLivingEffects.remove(effect) }, |
| 248 | + receiveCancel: { longLivingEffects.remove(effect) } |
250 | 249 | ) |
251 | | - .map(TestAction.receive) |
| 250 | + .map { .init(origin: .receive($0), file: action.file, line: action.line) } |
252 | 251 | .eraseToEffect() |
253 | | - |
254 | 252 | }, |
255 | 253 | environment: () |
256 | 254 | ) |
257 | 255 | defer { self.state = store.state.value } |
258 | 256 |
|
259 | | - let viewStore = ViewStore( |
260 | | - store.scope(state: self.toLocalState, action: TestAction.send) |
261 | | - ) |
262 | | - |
263 | 257 | func assert(step: Step) { |
264 | 258 | var expectedState = toLocalState(snapshotState) |
265 | 259 |
|
|
301 | 295 | file: step.file, line: step.line |
302 | 296 | ) |
303 | 297 | } |
304 | | - viewStore.send(action) |
| 298 | + ViewStore( |
| 299 | + store.scope( |
| 300 | + state: self.toLocalState, |
| 301 | + action: { .init(origin: .send($0), file: step.file, line: step.line) } |
| 302 | + ) |
| 303 | + ) |
| 304 | + .send(action) |
305 | 305 | do { |
306 | 306 | try update(&expectedState) |
307 | 307 | } catch { |
|
404 | 404 | ) |
405 | 405 | } |
406 | 406 |
|
407 | | - let unfinishedActions = longLivingEffects.filter { !$0.value.isEmpty }.map { $0.key } |
408 | | - if unfinishedActions.count > 0 { |
409 | | - let initiatingActions = unfinishedActions.map { "• \($0)" }.joined(separator: "\n") |
410 | | - let pluralSuffix = unfinishedActions.count == 1 ? "" : "s" |
411 | | - |
| 407 | + for effect in longLivingEffects { |
412 | 408 | _XCTFail( |
413 | 409 | """ |
414 | | - Some effects are still running. All effects must complete by the end of the assertion. |
415 | | -
|
416 | | - The effects that are still running were started by the following action\(pluralSuffix): |
417 | | -
|
418 | | - \(initiatingActions) |
| 410 | + An effect returned for this action is still running. It must complete before the end of \ |
| 411 | + the assertion. … |
| 412 | +
|
| 413 | + To fix, inspect any effects the reducer returns for this action and ensure that all of \ |
| 414 | + them complete by the end of the test. There are a few reasons why an effect may not have \ |
| 415 | + completed: |
| 416 | +
|
| 417 | + • If an effect uses a scheduler (via "receive(on:)", "delay", "debounce", etc.), make \ |
| 418 | + sure that you wait enough time for the scheduler to perform the effect. If you are using \ |
| 419 | + a test scheduler, advance the scheduler so that the effects may complete, or consider \ |
| 420 | + using an immediate scheduler to immediately perform the effect instead. |
| 421 | +
|
| 422 | + • If you are returning a long-living effect (timers, notifications, subjects, etc.), \ |
| 423 | + then make sure those effects are torn down by marking the effect ".cancellable" and \ |
| 424 | + returning a corresponding cancellation effect ("Effect.cancel") from another action, or, \ |
| 425 | + if your effect is driven by a Combine subject, send it a completion. |
| 426 | + """, |
| 427 | + file: effect.file, |
| 428 | + line: effect.line |
| 429 | + ) |
| 430 | + } |
| 431 | + } |
419 | 432 |
|
420 | | - To fix you need to inspect the effects returned from the above action\(pluralSuffix) and \ |
421 | | - make sure that all of them are completed by the end of your assertion. There are a few \ |
422 | | - reasons why your effects may not have completed: |
| 433 | + private struct LongLivingEffect: Hashable { |
| 434 | + let id = UUID() |
| 435 | + let file: StaticString |
| 436 | + let line: UInt |
423 | 437 |
|
424 | | - • If you are using a scheduler in your effect, then make sure that you wait enough time \ |
425 | | - for the effect to finish. If you are using a test scheduler, then make sure you advance \ |
426 | | - the scheduler so that the effects complete. |
| 438 | + static func == (lhs: Self, rhs: Self) -> Bool { |
| 439 | + lhs.id == rhs.id |
| 440 | + } |
427 | 441 |
|
428 | | - • If you are using long-living effects (for example timers, notifications, etc.), then \ |
429 | | - ensure those effects are completed by returning an `Effect.cancel` effect from a \ |
430 | | - particular action in your reducer, and sending that action in the test. |
431 | | - """, |
432 | | - file: file, line: line |
433 | | - ) |
| 442 | + func hash(into hasher: inout Hasher) { |
| 443 | + self.id.hash(into: &hasher) |
434 | 444 | } |
435 | 445 | } |
436 | 446 | } |
|
581 | 591 | } |
582 | 592 | } |
583 | 593 |
|
584 | | - private enum TestAction { |
585 | | - case send(LocalAction) |
586 | | - case receive(Action) |
| 594 | + private struct TestAction { |
| 595 | + let origin: Origin |
| 596 | + let file: StaticString |
| 597 | + let line: UInt |
| 598 | + |
| 599 | + enum Origin { |
| 600 | + case send(LocalAction) |
| 601 | + case receive(Action) |
| 602 | + } |
587 | 603 | } |
588 | 604 | } |
589 | 605 |
|
|
0 commit comments