Skip to content

Commit dc6d151

Browse files
Beta support for Swift Testing, and other improvements. (#867)
* wip * wip * wip * wip * wip * tests * wip * migration guide * wip * wip * wip * Update Sources/SnapshotTesting/AssertSnapshot.swift * wip * formatting * wip * format * more * wip * fix * Make record mode opaque. * more docs * wip * wip * Added new 'failed' record strategy, and wrote some tests. * remove test artificats * wip * more docs * fix linux tests * more test fixes * test clean up * debugging * debug * wip * fix * fix tests * wip * wip * wip * make snapshot configuration optional * make snapshot configuration optional * clean up * fix * indent * typo * more clean up * record before difftool * more tests * clean up test code --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 4742060 commit dc6d151

39 files changed

+1593
-372
lines changed

README.md

+20-8
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,28 @@ Repeat test runs will load this reference and compare it with the runtime value.
3838
match, the test will fail and describe the difference. Failures can be inspected from Xcode's Report
3939
Navigator or by inspecting the file URLs of the failure.
4040

41-
You can record a new reference by setting the `record` parameter to `true` on the assertion or
42-
setting `isRecording` globally.
41+
You can record a new reference by customizing snapshots inline with the assertion, or using the
42+
`withSnapshotTesting` tool:
4343

44-
``` swift
45-
assertSnapshot(of: vc, as: .image, record: true)
46-
47-
// or globally
44+
```swift
45+
// Record just this one snapshot
46+
assertSnapshot(of: vc, as: .image, record: .all)
47+
48+
// Record all snapshots in a scope:
49+
withSnapshotTesting(record: .all) {
50+
assertSnapshot(of: vc1, as: .image)
51+
assertSnapshot(of: vc2, as: .image)
52+
assertSnapshot(of: vc3, as: .image)
53+
}
4854

49-
isRecording = true
50-
assertSnapshot(of: vc, as: .image)
55+
// Record all snapshots in an XCTestCase subclass:
56+
class FeatureTests: XCTestCase {
57+
override func invokeTest() {
58+
withSnapshotTesting(record: .all) {
59+
super.invokeTest()
60+
}
61+
}
62+
}
5163
```
5264

5365
## Snapshot Anything

Sources/InlineSnapshotTesting/AssertInlineSnapshot.swift

+98-75
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
#if canImport(SwiftSyntax509)
4-
import SnapshotTesting
4+
@_spi(Internals) import SnapshotTesting
55
import SwiftParser
66
import SwiftSyntax
77
import SwiftSyntaxBuilder
@@ -35,7 +35,7 @@ import Foundation
3535
of value: @autoclosure () throws -> Value?,
3636
as snapshotting: Snapshotting<Value, String>,
3737
message: @autoclosure () -> String = "",
38-
record isRecording: Bool = isRecording,
38+
record isRecording: Bool? = nil,
3939
timeout: TimeInterval = 5,
4040
syntaxDescriptor: InlineSnapshotSyntaxDescriptor = InlineSnapshotSyntaxDescriptor(),
4141
matches expected: (() -> String)? = nil,
@@ -44,95 +44,116 @@ import Foundation
4444
line: UInt = #line,
4545
column: UInt = #column
4646
) {
47-
let _: Void = installTestObserver
48-
do {
49-
var actual: String?
50-
let expectation = XCTestExpectation()
51-
if let value = try value() {
52-
snapshotting.snapshot(value).run {
53-
actual = $0
54-
expectation.fulfill()
47+
let record =
48+
(isRecording == true ? .all : isRecording == false ? .missing : nil)
49+
?? SnapshotTestingConfiguration.current?.record
50+
?? _record
51+
withSnapshotTesting(record: record) {
52+
let _: Void = installTestObserver
53+
do {
54+
var actual: String?
55+
let expectation = XCTestExpectation()
56+
if let value = try value() {
57+
snapshotting.snapshot(value).run {
58+
actual = $0
59+
expectation.fulfill()
60+
}
61+
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
62+
case .completed:
63+
break
64+
case .timedOut:
65+
recordIssue(
66+
"""
67+
Exceeded timeout of \(timeout) seconds waiting for snapshot.
68+
69+
This can happen when an asynchronously loaded value (like a network response) has not \
70+
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
71+
"assertInlineSnapshot" to a higher value.
72+
""",
73+
file: file,
74+
line: line
75+
)
76+
return
77+
case .incorrectOrder, .interrupted, .invertedFulfillment:
78+
recordIssue("Couldn't snapshot value", file: file, line: line)
79+
return
80+
@unknown default:
81+
recordIssue("Couldn't snapshot value", file: file, line: line)
82+
return
83+
}
5584
}
56-
switch XCTWaiter.wait(for: [expectation], timeout: timeout) {
57-
case .completed:
58-
break
59-
case .timedOut:
60-
XCTFail(
85+
let expected = expected?()
86+
guard
87+
record != .all,
88+
record != .missing || expected != nil
89+
else {
90+
// NB: Write snapshot state before calling `XCTFail` in case `continueAfterFailure = false`
91+
inlineSnapshotState[File(path: file), default: []].append(
92+
InlineSnapshot(
93+
expected: expected,
94+
actual: actual,
95+
wasRecording: record == .all,
96+
syntaxDescriptor: syntaxDescriptor,
97+
function: "\(function)",
98+
line: line,
99+
column: column
100+
)
101+
)
102+
103+
var failure: String
104+
if syntaxDescriptor.trailingClosureLabel
105+
== InlineSnapshotSyntaxDescriptor.defaultTrailingClosureLabel
106+
{
107+
failure = "Automatically recorded a new snapshot."
108+
} else {
109+
failure = """
110+
Automatically recorded a new snapshot for "\(syntaxDescriptor.trailingClosureLabel)".
111+
"""
112+
}
113+
if let difference = snapshotting.diffing.diff(expected ?? "", actual ?? "")?.0 {
114+
failure += " Difference: …\n\n\(difference.indenting(by: 2))"
115+
}
116+
recordIssue(
61117
"""
62-
Exceeded timeout of \(timeout) seconds waiting for snapshot.
118+
\(failure)
63119
64-
This can happen when an asynchronously loaded value (like a network response) has not \
65-
loaded. If a timeout is unavoidable, consider setting the "timeout" parameter of
66-
"assertInlineSnapshot" to a higher value.
120+
Re-run "\(function)" to assert against the newly-recorded snapshot.
67121
""",
68122
file: file,
69123
line: line
70124
)
71125
return
72-
case .incorrectOrder, .interrupted, .invertedFulfillment:
73-
XCTFail("Couldn't snapshot value", file: file, line: line)
74-
return
75-
@unknown default:
76-
XCTFail("Couldn't snapshot value", file: file, line: line)
77-
return
78126
}
79-
}
80-
let expected = expected?()
81-
guard !isRecording, let expected
82-
else {
83-
// NB: Write snapshot state before calling `XCTFail` in case `continueAfterFailure = false`
84-
inlineSnapshotState[File(path: file), default: []].append(
85-
InlineSnapshot(
86-
expected: expected,
87-
actual: actual,
88-
wasRecording: isRecording,
89-
syntaxDescriptor: syntaxDescriptor,
90-
function: "\(function)",
91-
line: line,
92-
column: column
93-
)
94-
)
95127

96-
var failure: String
97-
if syntaxDescriptor.trailingClosureLabel
98-
== InlineSnapshotSyntaxDescriptor.defaultTrailingClosureLabel
99-
{
100-
failure = "Automatically recorded a new snapshot."
101-
} else {
102-
failure = """
103-
Automatically recorded a new snapshot for "\(syntaxDescriptor.trailingClosureLabel)".
128+
guard let expected
129+
else {
130+
recordIssue(
104131
"""
132+
No expected value to assert against.
133+
""",
134+
file: file,
135+
line: line
136+
)
137+
return
105138
}
106-
if let difference = snapshotting.diffing.diff(expected ?? "", actual ?? "")?.0 {
107-
failure += " Difference: …\n\n\(difference.indenting(by: 2))"
108-
}
109-
XCTFail(
139+
guard
140+
let difference = snapshotting.diffing.diff(expected, actual ?? "")?.0
141+
else { return }
142+
143+
let message = message()
144+
syntaxDescriptor.fail(
110145
"""
111-
\(failure)
146+
\(message.isEmpty ? "Snapshot did not match. Difference: …" : message)
112147
113-
Re-run "\(function)" to assert against the newly-recorded snapshot.
148+
\(difference.indenting(by: 2))
114149
""",
115150
file: file,
116-
line: line
151+
line: line,
152+
column: column
117153
)
118-
return
154+
} catch {
155+
recordIssue("Threw error: \(error)", file: file, line: line)
119156
}
120-
guard let difference = snapshotting.diffing.diff(expected, actual ?? "")?.0
121-
else { return }
122-
123-
let message = message()
124-
syntaxDescriptor.fail(
125-
"""
126-
\(message.isEmpty ? "Snapshot did not match. Difference: …" : message)
127-
128-
\(difference.indenting(by: 2))
129-
""",
130-
file: file,
131-
line: line,
132-
column: column
133-
)
134-
} catch {
135-
XCTFail("Threw error: \(error)", file: file, line: line)
136157
}
137158
}
138159
#else
@@ -197,6 +218,8 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
197218
/// Initializes an inline snapshot syntax descriptor.
198219
///
199220
/// - Parameters:
221+
/// - deprecatedTrailingClosureLabels: An array of deprecated labels to consider for the inline
222+
/// snapshot.
200223
/// - trailingClosureLabel: The label of the trailing closure that returns the inline snapshot.
201224
/// - trailingClosureOffset: The offset of the trailing closure that returns the inline
202225
/// snapshot, relative to the first trailing closure.
@@ -242,7 +265,7 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
242265
visitor.walk(testSource.sourceFile)
243266
trailingClosureLine = visitor.trailingClosureLine
244267
}
245-
XCTFail(
268+
recordIssue(
246269
message(),
247270
file: file,
248271
line: trailingClosureLine.map(UInt.init) ?? line
@@ -386,7 +409,7 @@ public struct InlineSnapshotSyntaxDescriptor: Hashable {
386409
) {
387410
self.file = file
388411
self.line = snapshots.first?.line
389-
self.wasRecording = snapshots.first?.wasRecording ?? isRecording
412+
self.wasRecording = snapshots.first?.wasRecording ?? false
390413
self.indent = String(
391414
sourceLocationConverter.sourceLines
392415
.first { $0.first?.isWhitespace == true && $0.contains { !$0.isWhitespace } }?

0 commit comments

Comments
 (0)