Skip to content

Commit b075eb1

Browse files
Add TestStore.assert. (#2123)
* Add TestStore.assert. * wip * Update Sources/ComposableArchitecture/TestStore.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md Co-authored-by: Stephen Celis <[email protected]> * fix tests --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent d6e1c09 commit b075eb1

File tree

3 files changed

+107
-0
lines changed

3 files changed

+107
-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(_:file:line:)``
2324
- ``finish(timeout:file:line:)``
2425
- ``TestStoreTask``
2526

Sources/ComposableArchitecture/TestStore.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,6 +1053,56 @@ 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+
/// > Note: This helper is only intended to be used with non-exhaustive test stores. It is not
1079+
/// needed in exhaustive test stores since any assertion you may make inside the trailing closure
1080+
/// has already been handled by a previous `send` or `receive`.
1081+
///
1082+
/// - Parameters:
1083+
/// - updateStateToExpectedResult: A closure that asserts against the current state of the test
1084+
/// store.
1085+
@MainActor
1086+
public func assert(
1087+
_ updateStateToExpectedResult: ((inout ScopedState) throws -> Void)?,
1088+
file: StaticString = #file,
1089+
line: UInt = #line
1090+
) {
1091+
let expectedState = self.toScopedState(self.state)
1092+
let currentState = self.reducer.state
1093+
do {
1094+
try self.expectedStateShouldMatch(
1095+
expected: expectedState,
1096+
actual: self.toScopedState(currentState),
1097+
updateStateToExpectedResult: updateStateToExpectedResult,
1098+
file: file,
1099+
line: line
1100+
)
1101+
} catch {
1102+
XCTFail("Threw error: \(error)", file: file, line: line)
1103+
}
1104+
}
1105+
10561106
/// Sends an action to the store and asserts when state changes.
10571107
///
10581108
/// This method returns a ``TestStoreTask``, which represents the lifecycle of the effect started

Tests/ComposableArchitectureTests/TestStoreTests.swift

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

436492
private struct Client: DependencyKey {

0 commit comments

Comments
 (0)