Skip to content

Commit 67a510d

Browse files
committed
Fix logic for new 'expected state to change' failure. (#1132)
* Fix logic for new 'expected state to change' failure. * clean up
1 parent f92485c commit 67a510d

File tree

4 files changed

+242
-126
lines changed

4 files changed

+242
-126
lines changed

Sources/ComposableArchitecture/TestSupport/TestStore.swift

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -376,54 +376,32 @@
376376
self.state = previousState
377377
defer { self.state = currentState }
378378

379-
try self.expectedStateShouldChange(
379+
try self.expectedStateShouldMatch(
380380
expected: &expectedState,
381+
actual: self.toLocalState(currentState),
381382
modify: updateExpectingResult,
382383
file: file,
383384
line: line
384385
)
385386
} catch {
386387
XCTFail("Threw error: \(error)", file: file, line: line)
387388
}
388-
self.expectedStateShouldMatch(
389-
expected: expectedState,
390-
actual: self.toLocalState(self.state),
391-
file: file,
392-
line: line
393-
)
394389
if "\(self.file)" == "\(file)" {
395390
self.line = line
396391
}
397392
}
398393

399-
private func expectedStateShouldChange(
394+
private func expectedStateShouldMatch(
400395
expected: inout LocalState,
396+
actual: LocalState,
401397
modify: ((inout LocalState) throws -> Void)? = nil,
402398
file: StaticString,
403399
line: UInt
404400
) throws {
405401
guard let modify = modify else { return }
406402
let current = expected
407403
try modify(&expected)
408-
if expected == current {
409-
XCTFail(
410-
"""
411-
Expected state to change, but no change occurred.
412-
413-
The trailing closure made no observable modifications to state. If no change to state is \
414-
expected, omit the trailing closure.
415-
""",
416-
file: file, line: line
417-
)
418-
}
419-
}
420404

421-
private func expectedStateShouldMatch(
422-
expected: LocalState,
423-
actual: LocalState,
424-
file: StaticString,
425-
line: UInt
426-
) {
427405
if expected != actual {
428406
let difference =
429407
diff(expected, actual, format: .proportional)
@@ -445,6 +423,16 @@
445423
file: file,
446424
line: line
447425
)
426+
} else if expected == current {
427+
XCTFail(
428+
"""
429+
Expected state to change, but no change occurred.
430+
431+
The trailing closure made no observable modifications to state. If no change to state is \
432+
expected, omit the trailing closure.
433+
""",
434+
file: file, line: line
435+
)
448436
}
449437
}
450438
}
@@ -497,21 +485,16 @@
497485
}
498486
var expectedState = self.toLocalState(self.state)
499487
do {
500-
try self.expectedStateShouldChange(
488+
try expectedStateShouldMatch(
501489
expected: &expectedState,
490+
actual: self.toLocalState(state),
502491
modify: updateExpectingResult,
503492
file: file,
504493
line: line
505494
)
506495
} catch {
507496
XCTFail("Threw error: \(error)", file: file, line: line)
508497
}
509-
expectedStateShouldMatch(
510-
expected: expectedState,
511-
actual: self.toLocalState(state),
512-
file: file,
513-
line: line
514-
)
515498
self.state = state
516499
if "\(self.file)" == "\(file)" {
517500
self.line = line

Tests/ComposableArchitectureTests/EffectTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ final class EffectTests: XCTestCase {
184184
XCTAssertEqual(result, 42)
185185
}
186186

187+
#if compiler(>=5.4) && !os(Linux)
188+
func testFailing() {
189+
let effect = Effect<Never, Never>.failing("failing")
190+
XCTExpectFailure {
191+
effect
192+
.start()
193+
} issueMatcher: { issue in
194+
issue.compactDescription == "failing - A failing effect ran."
195+
}
196+
}
197+
#endif
198+
187199
#if canImport(_Concurrency) && compiler(>=5.5.2)
188200
func testTask() {
189201
let expectation = self.expectation(description: "Complete")
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import ComposableArchitecture
2+
import XCTest
3+
4+
#if !os(Linux) // XCTExpectFailure is not supported on Linux
5+
class TestStoreFailureTests: XCTestCase {
6+
func testNoStateChangeFailure() {
7+
enum Action { case first, second }
8+
let store = TestStore(
9+
initialState: 0,
10+
reducer: Reducer<Int, Action, Void> { state, action, _ in
11+
switch action {
12+
case .first: return .init(value: .second)
13+
case .second: return .none
14+
}
15+
},
16+
environment: ()
17+
)
18+
19+
XCTExpectFailure {
20+
store.send(.first) { _ = $0 }
21+
} issueMatcher: {
22+
$0.compactDescription == """
23+
Expected state to change, but no change occurred.
24+
25+
The trailing closure made no observable modifications to state. If no change to state is \
26+
expected, omit the trailing closure.
27+
"""
28+
}
29+
30+
XCTExpectFailure {
31+
store.receive(.second) { _ = $0 }
32+
} issueMatcher: {
33+
$0.compactDescription == """
34+
Expected state to change, but no change occurred.
35+
36+
The trailing closure made no observable modifications to state. If no change to state is \
37+
expected, omit the trailing closure.
38+
"""
39+
}
40+
}
41+
42+
func testStateChangeFailure() {
43+
struct State: Equatable { var count = 0 }
44+
let store = TestStore(
45+
initialState: .init(),
46+
reducer: Reducer<State, Void, Void> { state, action, _ in state.count += 1; return .none },
47+
environment: ()
48+
)
49+
50+
XCTExpectFailure {
51+
store.send(()) { $0.count = 0 }
52+
} issueMatcher: {
53+
$0.compactDescription == """
54+
A state change does not match expectation: …
55+
56+
− TestStoreFailureTests.State(count: 0)
57+
+ TestStoreFailureTests.State(count: 1)
58+
59+
(Expected: −, Actual: +)
60+
"""
61+
}
62+
}
63+
64+
func testReceivedActionAfterDeinit() {
65+
XCTExpectFailure {
66+
do {
67+
enum Action { case first, second }
68+
let store = TestStore(
69+
initialState: 0,
70+
reducer: Reducer<Int, Action, Void> { state, action, _ in
71+
switch action {
72+
case .first: return .init(value: .second)
73+
case .second: return .none
74+
}
75+
},
76+
environment: ()
77+
)
78+
store.send(.first)
79+
}
80+
} issueMatcher: {
81+
$0.compactDescription == """
82+
The store received 1 unexpected action after this one: …
83+
84+
Unhandled actions: [
85+
[0]: TestStoreFailureTests.Action.second
86+
]
87+
"""
88+
}
89+
}
90+
91+
func testEffectInFlightAfterDeinit() {
92+
XCTExpectFailure {
93+
do {
94+
let store = TestStore(
95+
initialState: 0,
96+
reducer: Reducer<Int, Void, Void> { state, action, _ in
97+
.task { try? await Task.sleep(nanoseconds: NSEC_PER_SEC) }
98+
},
99+
environment: ()
100+
)
101+
store.send(())
102+
}
103+
} issueMatcher: {
104+
$0.compactDescription == """
105+
An effect returned for this action is still running. It must complete before the end of the \
106+
test. …
107+
108+
To fix, inspect any effects the reducer returns for this action and ensure that all of \
109+
them complete by the end of the test. There are a few reasons why an effect may not have \
110+
completed:
111+
112+
• If an effect uses a scheduler (via "receive(on:)", "delay", "debounce", etc.), make sure \
113+
that you wait enough time for the scheduler to perform the effect. If you are using a test \
114+
scheduler, advance the scheduler so that the effects may complete, or consider using an \
115+
immediate scheduler to immediately perform the effect instead.
116+
117+
• If you are returning a long-living effect (timers, notifications, subjects, etc.), then \
118+
make sure those effects are torn down by marking the effect ".cancellable" and returning a \
119+
corresponding cancellation effect ("Effect.cancel") from another action, or, if your \
120+
effect is driven by a Combine subject, send it a completion.
121+
"""
122+
}
123+
}
124+
125+
func testSendActionBeforeReceivingFailure() {
126+
enum Action { case first, second }
127+
let store = TestStore(
128+
initialState: 0,
129+
reducer: Reducer<Int, Action, Void> { state, action, _ in
130+
switch action {
131+
case .first: return .init(value: .second)
132+
case .second: return .none
133+
}
134+
},
135+
environment: ()
136+
)
137+
138+
XCTExpectFailure {
139+
store.send(.first)
140+
store.send(.first)
141+
store.receive(.second)
142+
store.receive(.second)
143+
} issueMatcher: { issue in
144+
issue.compactDescription == """
145+
Must handle 1 received action before sending an action: …
146+
147+
Unhandled actions: [
148+
[0]: TestStoreFailureTests.Action.second
149+
]
150+
"""
151+
}
152+
}
153+
154+
func testReceiveNonExistentActionFailure() {
155+
enum Action { case action }
156+
let store = TestStore(
157+
initialState: 0,
158+
reducer: Reducer<Int, Action, Void> { _, _, _ in .none },
159+
environment: ()
160+
)
161+
162+
XCTExpectFailure {
163+
store.receive(.action)
164+
} issueMatcher: { issue in
165+
issue.compactDescription == "Expected to receive an action, but received none."
166+
}
167+
}
168+
169+
func testReceiveUnexpectedActionFailure() {
170+
enum Action { case first, second }
171+
let store = TestStore(
172+
initialState: 0,
173+
reducer: Reducer<Int, Action, Void> { state, action, _ in
174+
switch action {
175+
case .first: return .init(value: .second)
176+
case .second: return .none
177+
}
178+
},
179+
environment: ()
180+
)
181+
182+
XCTExpectFailure {
183+
store.send(.first)
184+
store.receive(.first)
185+
} issueMatcher: { issue in
186+
issue.compactDescription == """
187+
Received unexpected action: …
188+
189+
− TestStoreFailureTests.Action.first
190+
+ TestStoreFailureTests.Action.second
191+
192+
(Expected: −, Received: +)
193+
"""
194+
}
195+
}
196+
197+
func testModifyClosureThrowsErrorFailure() {
198+
let store = TestStore(
199+
initialState: 0,
200+
reducer: Reducer<Int, Void, Void> { _, _, _ in .none },
201+
environment: ()
202+
)
203+
204+
XCTExpectFailure {
205+
store.send(()) { _ in
206+
struct SomeError: Error {}
207+
throw SomeError()
208+
}
209+
} issueMatcher: { issue in
210+
issue.compactDescription == "Threw error: SomeError()"
211+
}
212+
}
213+
}
214+
#endif

0 commit comments

Comments
 (0)