Skip to content

Commit 056dea6

Browse files
Add XCTAssertDifference for exhaustively testing changes to values (#51)
* Add `XCTAssertDifference` for testing changes to values * wip * wip * Docs * Update Tests/CustomDumpTests/XCTAssertDifferenceTests.swift Co-authored-by: Brandon Williams <[email protected]> * wip * wip --------- Co-authored-by: Brandon Williams <[email protected]>
1 parent 8196799 commit 056dea6

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import XCTestDynamicOverlay
2+
3+
/// Asserts that a value has a set of changes.
4+
///
5+
/// This function evaluates a given expression before and after a given operation and then compares
6+
/// the results. The comparison is done by invoking the `changes` closure with a mutable version of
7+
/// the initial value, and then asserting that the modifications made match the final value using
8+
/// ``XCTAssertNoDifference``.
9+
///
10+
/// For example, given a very simple counter structure, we can write a test against its incrementing
11+
/// functionality:
12+
/// `
13+
/// ```swift
14+
/// struct Counter {
15+
/// var count = 0
16+
/// var isOdd = false
17+
/// mutating func increment() {
18+
/// self.count += 1
19+
/// self.isOdd.toggle()
20+
/// }
21+
/// }
22+
///
23+
/// var counter = Counter()
24+
/// XCTAssertDifference(counter) {
25+
/// counter.increment()
26+
/// } changes: {
27+
/// $0.count = 1
28+
/// $0.isOdd = true
29+
/// }
30+
/// ```
31+
///
32+
/// If the `changes` does not exhaustively describe all changed fields, the assertion will fail.
33+
///
34+
/// By omitting the operation you can write a "non-exhaustive" assertion against a value by
35+
/// describing just the fields you want to assert against in the `changes` closure:
36+
///
37+
/// ```swift
38+
/// counter.increment()
39+
/// XCTAssertDifference(counter) {
40+
/// $0.count = 1
41+
/// // Don't need to further describe how `isOdd` has changed
42+
/// }
43+
/// ```
44+
///
45+
/// - Parameters:
46+
/// - expression: An expression that is evaluated before and after `operation`, and then compared.
47+
/// - message: An optional description of a failure.
48+
/// - operation: An optional operation that is performed in between an initial and final
49+
/// evaluation of `operation`. By omitting this operation, you can write a "non-exhaustive"
50+
/// assertion against an already-changed value by describing just the fields you want to assert
51+
/// against in the `changes` closure.
52+
/// - updateExpectingResult: A closure that asserts how the expression changed by supplying a
53+
/// mutable version of the initial value. This value must be modified to match the final value.
54+
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
55+
public func XCTAssertDifference<T>(
56+
_ expression: @autoclosure () throws -> T,
57+
_ message: @autoclosure () -> String = "",
58+
operation: () throws -> Void = {},
59+
changes updateExpectingResult: (inout T) throws -> Void,
60+
file: StaticString = #filePath,
61+
line: UInt = #line
62+
) where T: Equatable {
63+
do {
64+
var expression1 = try expression()
65+
try updateExpectingResult(&expression1)
66+
try operation()
67+
let expression2 = try expression()
68+
let message = message()
69+
guard expression1 != expression2 else { return }
70+
let format = DiffFormat.proportional
71+
guard let difference = diff(expression1, expression2, format: format)
72+
else {
73+
XCTFail(
74+
"""
75+
XCTAssertDifference failed: ("\(expression1)" is not equal to ("\(expression2)"), but no \
76+
difference was detected.
77+
""",
78+
file: file,
79+
line: line
80+
)
81+
return
82+
}
83+
let failure = """
84+
XCTAssertDifference failed: …
85+
86+
\(difference.indenting(by: 2))
87+
88+
(Expected: \(format.first), Actual: \(format.second))
89+
"""
90+
XCTFail(
91+
"\(failure)\(message.isEmpty ? "" : " - \(message)")",
92+
file: file,
93+
line: line
94+
)
95+
} catch {
96+
XCTFail(
97+
"""
98+
XCTAssertDifference failed: threw error "\(error)"
99+
""",
100+
file: file,
101+
line: line
102+
)
103+
}
104+
}
105+
106+
/// Asserts that a value has a set of changes.
107+
///
108+
/// An async version of ``XCTAssertDifference(_:_:operation:changes:)``.
109+
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
110+
public func XCTAssertDifference<T: Sendable>(
111+
_ expression: @autoclosure @Sendable () throws -> T,
112+
_ message: @autoclosure @Sendable () -> String = "",
113+
operation: @Sendable () async throws -> Void = {},
114+
changes updateExpectingResult: @Sendable (inout T) throws -> Void,
115+
file: StaticString = #filePath,
116+
line: UInt = #line
117+
) async where T: Equatable {
118+
do {
119+
var expression1 = try expression()
120+
try updateExpectingResult(&expression1)
121+
try await operation()
122+
let expression2 = try expression()
123+
let message = message()
124+
guard expression1 != expression2 else { return }
125+
let format = DiffFormat.proportional
126+
guard let difference = diff(expression1, expression2, format: format)
127+
else {
128+
XCTFail(
129+
"""
130+
XCTAssertDifference failed: ("\(expression1)" is not equal to ("\(expression2)"), but no \
131+
difference was detected.
132+
""",
133+
file: file,
134+
line: line
135+
)
136+
return
137+
}
138+
let failure = """
139+
XCTAssertDifference failed: …
140+
141+
\(difference.indenting(by: 2))
142+
143+
(Expected: \(format.first), Actual: \(format.second))
144+
"""
145+
XCTFail(
146+
"\(failure)\(message.isEmpty ? "" : " - \(message)")",
147+
file: file,
148+
line: line
149+
)
150+
} catch {
151+
XCTFail(
152+
"""
153+
XCTAssertDifference failed: threw error "\(error)"
154+
""",
155+
file: file,
156+
line: line
157+
)
158+
}
159+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import CustomDump
2+
import XCTest
3+
4+
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
5+
class XCTAssertDifferencesTests: XCTestCase {
6+
func testXCTAssertDifference() {
7+
var user = User(id: 42, name: "Blob")
8+
func increment<Value>(_ root: inout Value, at keyPath: WritableKeyPath<Value, Int>) {
9+
root[keyPath: keyPath] += 1
10+
}
11+
12+
XCTAssertDifference(user) {
13+
increment(&user, at: \.id)
14+
} changes: {
15+
$0.id = 43
16+
}
17+
}
18+
19+
func testXCTAssertDifference_NonExhaustive() {
20+
let user = User(id: 42, name: "Blob")
21+
22+
XCTAssertDifference(user) {
23+
$0.id = 42
24+
$0.name = "Blob"
25+
}
26+
}
27+
28+
#if DEBUG && compiler(>=5.4) && (os(iOS) || os(macOS) || os(tvOS) || os(watchOS))
29+
func testXCTAssertDifference_Failure() {
30+
var user = User(id: 42, name: "Blob")
31+
func increment<Value>(_ root: inout Value, at keyPath: WritableKeyPath<Value, Int>) {
32+
root[keyPath: keyPath] += 1
33+
}
34+
35+
XCTExpectFailure()
36+
37+
XCTAssertDifference(user) {
38+
increment(&user, at: \.id)
39+
} changes: {
40+
$0.id = 44
41+
}
42+
}
43+
#endif
44+
}

0 commit comments

Comments
 (0)