Skip to content

Commit a892ccc

Browse files
committed
Add TestStore.assert.
1 parent 43718e0 commit a892ccc

File tree

3 files changed

+99
-0
lines changed

3 files changed

+99
-0
lines changed

Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
- ``receive(_:timeout:assert:file:line:)-1rwdd``
2121
- ``receive(_:timeout:assert:file:line:)-8xkqt``
2222
- ``receive(_:timeout:assert:file:line:)-2ju31``
23+
- ``assert(assert:file:line:)``
2324
- ``finish(timeout:file:line:)``
2425
- ``TestStoreTask``
2526

Sources/ComposableArchitecture/TestStore.swift

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,52 @@ extension TestStore where ScopedState: Equatable {
10531053
return .init(rawValue: task, timeout: self.timeout)
10541054
}
10551055

1056+
/// Assert against the current state of the store.
1057+
///
1058+
/// The trailing closure provided is given a mutable argument that represents the current state,
1059+
/// and you can provide any mutations you want to the state. If your mutations cause the argument
1060+
/// to differ from the current state of the test store, a test failure will be triggered.
1061+
///
1062+
/// This tool is most useful in non-exhaustive test stores (see
1063+
/// <doc:Testing#Non-exhaustive-testing>), which allow you to assert on a subset of the things
1064+
/// happening inside your features. For example, you can send an action in a child feature
1065+
/// without asserting on how many changes in the system, and then tell the test store to
1066+
/// ``finish(timeout:file:line:)`` by executing all of its effects and receiving all actions.
1067+
/// After that is done you can assert on the final state of the store:
1068+
///
1069+
/// ```swift
1070+
/// store.exhaustivity = .off
1071+
/// await store.send(.child(.closeButtonTapped))
1072+
/// await store.finish()
1073+
/// store.assert {
1074+
/// $0.child = nil
1075+
/// }
1076+
/// ```
1077+
///
1078+
/// - Parameters:
1079+
/// - updateStateToExpectedResult: A closure that asserts against the current state of the test
1080+
/// store.
1081+
@MainActor
1082+
public func assert(
1083+
assert updateStateToExpectedResult: ((inout ScopedState) throws -> Void)?,
1084+
file: StaticString = #file,
1085+
line: UInt = #line
1086+
) {
1087+
let expectedState = self.toScopedState(self.state)
1088+
let currentState = self.reducer.state
1089+
do {
1090+
try self.expectedStateShouldMatch(
1091+
expected: expectedState,
1092+
actual: self.toScopedState(currentState),
1093+
updateStateToExpectedResult: updateStateToExpectedResult,
1094+
file: file,
1095+
line: line
1096+
)
1097+
} catch {
1098+
XCTFail("Threw error: \(error)", file: file, line: line)
1099+
}
1100+
}
1101+
10561102
/// Sends an action to the store and asserts when state changes.
10571103
///
10581104
/// This method returns a ``TestStoreTask``, which represents the lifecycle of the effect started

Tests/ComposableArchitectureTests/TestStoreTests.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,58 @@ final class TestStoreTests: BaseTCATestCase {
431431
$0 = 42
432432
}
433433
}
434+
435+
func testAssert_ExhaustiveTestStore() async {
436+
let store = TestStore(initialState: 0) {
437+
EmptyReducer<Int, Void>()
438+
}
439+
440+
XCTExpectFailure {
441+
store.assert {
442+
$0 = 0
443+
}
444+
} issueMatcher: {
445+
$0.compactDescription == """
446+
Expected state to change, but no change occurred.
447+
448+
The trailing closure made no observable modifications to state. If no change to state is \
449+
expected, omit the trailing closure.
450+
"""
451+
}
452+
}
453+
454+
func testAssert_NonExhaustiveTestStore() async {
455+
let store = TestStore(initialState: 0) {
456+
EmptyReducer<Int, Void>()
457+
}
458+
store.exhaustivity = .off
459+
460+
store.assert {
461+
$0 = 0
462+
}
463+
}
464+
465+
func testAssert_NonExhaustiveTestStore_Failure() async {
466+
let store = TestStore(initialState: 0) {
467+
EmptyReducer<Int, Void>()
468+
}
469+
store.exhaustivity = .off
470+
471+
XCTExpectFailure {
472+
store.assert {
473+
$0 = 1
474+
}
475+
} issueMatcher: {
476+
$0.compactDescription == """
477+
A state change does not match expectation: …
478+
479+
− 1
480+
+ 0
481+
482+
(Expected: −, Actual: +)
483+
"""
484+
}
485+
}
434486
}
435487

436488
private struct Client: DependencyKey {

0 commit comments

Comments
 (0)