Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't show 'expected state to change' error in non-exhaustive test stores. #2227

Merged
merged 2 commits into from
Jun 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,17 @@ extension EffectPublisher where Failure == Never {
}

@usableFromInline
func debugCaseOutput(_ value: Any) -> String {
func debugCaseOutput(
_ value: Any,
abbreviated: Bool = false
) -> String {
func debugCaseOutputHelp(_ value: Any) -> String {
let mirror = Mirror(reflecting: value)
switch mirror.displayStyle {
case .enum:
guard let child = mirror.children.first else {
let childOutput = "\(value)"
return childOutput == "\(type(of: value))" ? "" : ".\(childOutput)"
return childOutput == "\(typeName(type(of: value)))" ? "" : ".\(childOutput)"
}
let childOutput = debugCaseOutputHelp(child.value)
return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")"
Expand All @@ -173,7 +176,7 @@ func debugCaseOutput(_ value: Any) -> String {
}

return (value as? CustomDebugStringConvertible)?.debugDescription
?? "\(typeName(type(of: value)))\(debugCaseOutputHelp(value))"
?? "\(abbreviated ? "" : typeName(type(of: value)))\(debugCaseOutputHelp(value))"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Snuck another change in this PR to shorten the action description when you get a test failure telling you there are actions that were not yet received.

}

private func isUnlabeledArgument(_ label: String) -> Bool {
Expand Down
18 changes: 13 additions & 5 deletions Sources/ComposableArchitecture/TestStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -879,14 +879,17 @@ public final class TestStore<State, Action, ScopedState, ScopedAction, Environme

func completed() {
if !self.reducer.receivedActions.isEmpty {
var actions = ""
customDump(self.reducer.receivedActions.map(\.action), to: &actions)
let actions = self.reducer.receivedActions
.map(\.action)
.map { " • " + debugCaseOutput($0, abbreviated: true) }
.joined(separator: "\n")
XCTFailHelper(
"""
The store received \(self.reducer.receivedActions.count) unexpected \
action\(self.reducer.receivedActions.count == 1 ? "" : "s") after this one: …

Unhandled actions: \(actions)
Unhandled actions:
\(actions)
""",
file: self.file,
line: self.line
Expand Down Expand Up @@ -1163,7 +1166,7 @@ extension TestStore where ScopedState: Equatable {
/// store.
@MainActor
public func assert(
_ updateStateToExpectedResult: ((inout ScopedState) throws -> Void)?,
_ updateStateToExpectedResult: @escaping (inout ScopedState) throws -> Void,
file: StaticString = #file,
line: UInt = #line
) {
Expand All @@ -1174,6 +1177,7 @@ extension TestStore where ScopedState: Equatable {
expected: expectedState,
actual: self.toScopedState(currentState),
updateStateToExpectedResult: updateStateToExpectedResult,
skipUnnecessaryModifyFailure: true,
file: file,
line: line
)
Expand Down Expand Up @@ -1278,6 +1282,7 @@ extension TestStore where ScopedState: Equatable {
expected: ScopedState,
actual: ScopedState,
updateStateToExpectedResult: ((inout ScopedState) throws -> Void)? = nil,
skipUnnecessaryModifyFailure: Bool = false,
file: StaticString,
line: UInt
) throws {
Expand Down Expand Up @@ -1395,7 +1400,10 @@ extension TestStore where ScopedState: Equatable {
}

func tryUnnecessaryModifyFailure() {
guard expected == current && updateStateToExpectedResult != nil
guard
!skipUnnecessaryModifyFailure,
expected == current,
updateStateToExpectedResult != nil
else { return }

XCTFailHelper(
Expand Down
5 changes: 2 additions & 3 deletions Tests/ComposableArchitectureTests/TestStoreFailureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,8 @@
$0.compactDescription == """
The store received 1 unexpected action after this one: …

Unhandled actions: [
[0]: .second
]
Unhandled actions:
• .second
"""
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,15 @@
await store.receive(.response2, timeout: 1_000_000_000)
}

func testNonExhaustive_ShowSkippedAssertions_ExpectedStateToChange() async {
let store = TestStore(initialState: 0) {
Reduce<Int, Void> { _, _ in .none }
}
store.exhaustivity = .off(showSkippedAssertions: true)
await store.send(()) { $0 = 0 }
store.assert { $0 = 0 }
}

// This example comes from Krzysztof Zabłocki's blog post:
// https://www.merowing.info/exhaustive-testing-in-tca/
func testKrzysztofExample1() {
Expand Down
21 changes: 0 additions & 21 deletions Tests/ComposableArchitectureTests/TestStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -432,27 +432,6 @@ final class TestStoreTests: BaseTCATestCase {
}
}

#if DEBUG
func testAssert_ExhaustiveTestStore() async {
let store = TestStore(initialState: 0) {
EmptyReducer<Int, Void>()
}

XCTExpectFailure {
store.assert {
$0 = 0
}
} issueMatcher: {
$0.compactDescription == """
Expected state to change, but no change occurred.

The trailing closure made no observable modifications to state. If no change to state is \
expected, omit the trailing closure.
"""
}
}
#endif

func testAssert_NonExhaustiveTestStore() async {
let store = TestStore(initialState: 0) {
EmptyReducer<Int, Void>()
Expand Down