Skip to content

Commit 8a4f3ad

Browse files
authored
Promote CustomTestReflectable to API. (#1686)
Per [ST-0022](https://github.com/swiftlang/swift-evolution/blob/main/proposals/testing/0022-customtestreflectable.md), promote this interface to API. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent fa5a211 commit 8a4f3ad

6 files changed

Lines changed: 199 additions & 69 deletions

File tree

Sources/Testing/SourceAttribution/CustomTestReflectable.swift

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,27 @@
1313
///
1414
/// ## See Also
1515
///
16-
/// - ``Swift/Mirror/init(reflectingForTest:)``
17-
@_spi(Experimental)
16+
/// - ``Swift/Mirror/init(reflectingForTest:)-(Any)``
17+
/// - ``Swift/Mirror/init(reflectingForTest:)-(CustomTestReflectable)``
18+
///
19+
/// @Metadata {
20+
/// @Available(Swift, introduced: 6.4)
21+
/// }
1822
public protocol CustomTestReflectable {
1923
/// The custom mirror for this instance.
2024
///
2125
/// Do not use this property directly. To get the test reflection of a value,
22-
/// use ``Swift/Mirror/init(reflectingForTest:)``.
26+
/// use ``Swift/Mirror/init(reflectingForTest:)-(CustomTestReflectable)``.
27+
///
28+
/// @Metadata {
29+
/// @Available(Swift, introduced: 6.4)
30+
/// }
2331
var customTestMirror: Mirror { get }
2432
}
2533

26-
@_spi(Experimental)
34+
/// @Metadata {
35+
/// @Available(Swift, introduced: 6.4)
36+
/// }
2737
extension Mirror {
2838
/// Initialize this instance so that it can be presented in a test's output.
2939
///
@@ -33,6 +43,10 @@ extension Mirror {
3343
/// ## See Also
3444
///
3545
/// - ``CustomTestReflectable``
46+
///
47+
/// @Metadata {
48+
/// @Available(Swift, introduced: 6.4)
49+
/// }
3650
public init(reflectingForTest subject: some CustomTestReflectable) {
3751
self = subject.customTestMirror
3852
}
@@ -45,6 +59,10 @@ extension Mirror {
4559
/// ## See Also
4660
///
4761
/// - ``CustomTestReflectable``
62+
///
63+
/// @Metadata {
64+
/// @Available(Swift, introduced: 6.4)
65+
/// }
4866
public init(reflectingForTest subject: some Any) {
4967
if let subject = subject as? any CustomTestReflectable {
5068
self.init(reflectingForTest: subject)

Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,69 +11,6 @@
1111
/// A protocol describing types with a custom string representation when
1212
/// presented as part of a test's output.
1313
///
14-
/// Values whose types conform to this protocol use it to describe themselves
15-
/// when they are present as part of the output of a test. For example, this
16-
/// protocol affects the display of values that are passed as arguments to test
17-
/// functions or that are elements of an expectation failure.
18-
///
19-
/// By default, the testing library converts values to strings using
20-
/// `String(describing:)`. The resulting string may be inappropriate for some
21-
/// types and their values. If the type of the value is made to conform to
22-
/// ``CustomTestStringConvertible``, then the value of its ``testDescription``
23-
/// property will be used instead.
24-
///
25-
/// For example, consider the following type:
26-
///
27-
/// ```swift
28-
/// enum Food: CaseIterable {
29-
/// case paella, oden, ragu
30-
/// }
31-
/// ```
32-
///
33-
/// If an array of cases from this enumeration is passed to a parameterized test
34-
/// function:
35-
///
36-
/// ```swift
37-
/// @Test(arguments: Food.allCases)
38-
/// func isDelicious(_ food: Food) { ... }
39-
/// ```
40-
///
41-
/// Then the values in the array need to be presented in the test output, but
42-
/// the default description of a value may not be adequately descriptive:
43-
///
44-
/// ```
45-
/// ◇ Test case passing 1 argument food → .paella to isDelicious(_:) started.
46-
/// ◇ Test case passing 1 argument food → .oden to isDelicious(_:) started.
47-
/// ◇ Test case passing 1 argument food → .ragu to isDelicious(_:) started.
48-
/// ```
49-
///
50-
/// By adopting ``CustomTestStringConvertible``, customized descriptions can be
51-
/// included:
52-
///
53-
/// ```swift
54-
/// extension Food: CustomTestStringConvertible {
55-
/// var testDescription: String {
56-
/// switch self {
57-
/// case .paella:
58-
/// "paella valenciana"
59-
/// case .oden:
60-
/// "おでん"
61-
/// case .ragu:
62-
/// "ragù alla bolognese"
63-
/// }
64-
/// }
65-
/// }
66-
/// ```
67-
///
68-
/// The presentation of these values will then reflect the value of the
69-
/// ``testDescription`` property:
70-
///
71-
/// ```
72-
/// ◇ Test case passing 1 argument food → paella valenciana to isDelicious(_:) started.
73-
/// ◇ Test case passing 1 argument food → おでん to isDelicious(_:) started.
74-
/// ◇ Test case passing 1 argument food → ragù alla bolognese to isDelicious(_:) started.
75-
/// ```
76-
///
7714
/// ## See Also
7815
///
7916
/// - ``Swift/String/init(describingForTest:)``

Sources/Testing/Testing.docc/Documentation.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ their problems.
6868

6969
- <doc:Traits>
7070

71+
### Value description and reflection
72+
73+
- <doc:describing-values>
74+
- ``CustomTestReflectable``
75+
- ``CustomTestStringConvertible``
76+
7177
### Data collection
7278

7379
- <doc:Attachments>

Sources/Testing/Testing.docc/Expectations.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ the test when the code doesn't satisfy a requirement, use
9191

9292
- ``Expectation``
9393
- ``ExpectationFailedError``
94-
- ``CustomTestStringConvertible``
9594

9695
### Representing source locations
9796

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# Describing and reflecting values
2+
3+
<!--
4+
This source file is part of the Swift.org open source project
5+
6+
Copyright (c) 2024–2026 Apple Inc. and the Swift project authors
7+
Licensed under Apache License v2.0 with Runtime Library Exception
8+
9+
See https://swift.org/LICENSE.txt for license information
10+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
11+
-->
12+
13+
Add custom descriptions and mirrors to values you use in your tests.
14+
15+
## Overview
16+
17+
The testing library provides two protocols, ``CustomTestStringConvertible`` and
18+
``CustomTestReflectable``, that you can use to customize the appearance of
19+
values in Swift. The testing library uses these protocols to describe
20+
parameterized test arguments and, if a call to ``expect(_:_:sourceLocation:)``
21+
or ``require(_:_:sourceLocation:)-5l63q`` fails, to describe any values you pass
22+
to them.
23+
24+
## Customize the description of a value
25+
26+
You use the ``CustomTestStringConvertible`` protocol when you want to customize
27+
the description of a value _during testing only_. Values whose types conform to
28+
this protocol use it to describe themselves when the testing library presents
29+
them as part of the output of a test. For example, this protocol affects the
30+
display of values you pass as arguments to test functions or that are elements
31+
of an expectation failure.
32+
33+
By default, the testing library converts values to strings using
34+
[`String(describing:)`](https://developer.apple.com/documentation/swift/string/init(describing:)-67ncf).
35+
The resulting string may be inappropriate for some types and their values. If
36+
you make the type of the value conform to ``CustomTestStringConvertible``, then
37+
the testing library will use the value of its ``CustomTestStringConvertible/testDescription``
38+
property instead.
39+
40+
For example, consider the following type:
41+
42+
```swift
43+
enum Food: CaseIterable {
44+
case paella, oden, ragu
45+
}
46+
```
47+
48+
If you pass an array of cases from this enumeration to a parameterized test
49+
function:
50+
51+
```swift
52+
@Test(arguments: Food.allCases)
53+
func isDelicious(_ food: Food) { ... }
54+
```
55+
56+
Then the testing library needs to present all elements in the array in its
57+
output, but the default description of these values may not be adequately
58+
descriptive:
59+
60+
```
61+
◇ Test case passing 1 argument food → .paella to isDelicious(_:) started.
62+
◇ Test case passing 1 argument food → .oden to isDelicious(_:) started.
63+
◇ Test case passing 1 argument food → .ragu to isDelicious(_:) started.
64+
```
65+
66+
When you adopt ``CustomTestStringConvertible``, you can include customized
67+
descriptions in your test output instead.
68+
69+
```swift
70+
extension Food: CustomTestStringConvertible {
71+
var testDescription: String {
72+
switch self {
73+
case .paella:
74+
"paella valenciana"
75+
case .oden:
76+
"おでん"
77+
case .ragu:
78+
"ragù alla bolognese"
79+
}
80+
}
81+
}
82+
```
83+
84+
The testing library then uses ``CustomTestStringConvertible/testDescription`` to
85+
present these values:
86+
87+
```
88+
◇ Test case passing 1 argument food → paella valenciana to isDelicious(_:) started.
89+
◇ Test case passing 1 argument food → おでん to isDelicious(_:) started.
90+
◇ Test case passing 1 argument food → ragù alla bolognese to isDelicious(_:) started.
91+
```
92+
93+
## Customize the reflection of a value
94+
95+
When a call to ``expect(_:_:sourceLocation:)`` or to ``require(_:_:sourceLocation:)-5l63q``
96+
fails, the testing library presents the value or values you pass to these macros
97+
in its output.
98+
99+
The testing library uses [`Mirror.init(reflecting:)`](https://developer.apple.com/documentation/swift/mirror/init(reflecting:))
100+
to break down these values if they contain properties that may be of interest to
101+
you. For instance, if the `isDelicious(_:)` test fails, you might see output
102+
such as:
103+
104+
```
105+
✘ Test isDelicious(_:) recorded an issue with 1 argument food → sandwich
106+
↳ food.isDelicious → false
107+
↳ food → sandwich
108+
↳ sandwich → (toppings: [Food.pickles, Food.candyCorn])
109+
↳ toppings → [Food.pickles, Food.candyCorn]
110+
↳ isDelicious → false
111+
```
112+
113+
This output is expressive, but also contains redundant information. You can
114+
refine it further by making `Food` conform to the ``CustomTestReflectable``
115+
protocol.
116+
117+
```swift
118+
extension Food: CustomTestReflectable {
119+
var customTestMirror: Mirror {
120+
switch self {
121+
case let .sandwich(toppings):
122+
let ingredientNames = toppings.map { String(describingForTest: $0) }
123+
return Mirror(
124+
self,
125+
children: [(label: "toppings", value: ingredientNames)]
126+
)
127+
default:
128+
Mirror(self, children: [])
129+
}
130+
}
131+
}
132+
```
133+
134+
With this conformance, the output of the failed test is instead:
135+
136+
```
137+
✘ Test isDelicious(_:) recorded an issue with 1 argument food → sandwich
138+
↳ food.isDelicious → false
139+
↳ food → sandwich
140+
↳ toppings → ["pickles", "candy corn"]
141+
↳ isDelicious → false
142+
```
143+
144+
## Implement custom descriptions using private properties
145+
146+
If part or all of your type's state is `private` or otherwise not visible to
147+
your test target, you may not be able to implement ``CustomTestStringConvertible/testDescription``
148+
or ``CustomTestReflectable/customTestMirror`` in your test target. You can
149+
implement these properties, without adding conformances to either protocol, in
150+
your production target, and then add empty protocol conformances in your test
151+
target. Make sure to use `internal` or `package` visibility for the properties
152+
so that your test target is able to use them.
153+
154+
```swift
155+
// In your production target:
156+
157+
extension Food {
158+
package var testDescription: String { ... }
159+
package var customTestMirror: Mirror { ... }
160+
}
161+
162+
// In your test target:
163+
164+
import FoodTruck
165+
166+
extension Food: CustomTestStringConvertible, CustomTestReflectable {}
167+
```
168+
169+
- Note: If you use `internal` visibility for these properties, you must import
170+
your production target into your test target using the `@testable` attribute.

Tests/TestingTests/CustomTestReflectableTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
// See https://swift.org/CONTRIBUTORS.txt for Swift project authors
99
//
1010

11-
@testable @_spi(Experimental) import Testing
11+
@testable import Testing
1212

1313
struct `CustomTestReflectable Tests` {
1414
@Test func `Can get a custom mirror from a value`() throws {

0 commit comments

Comments
 (0)