diff --git a/Sources/Testing/SourceAttribution/CustomTestReflectable.swift b/Sources/Testing/SourceAttribution/CustomTestReflectable.swift index 1bb55d414..2c6a8eb61 100644 --- a/Sources/Testing/SourceAttribution/CustomTestReflectable.swift +++ b/Sources/Testing/SourceAttribution/CustomTestReflectable.swift @@ -13,17 +13,27 @@ /// /// ## See Also /// -/// - ``Swift/Mirror/init(reflectingForTest:)`` -@_spi(Experimental) +/// - ``Swift/Mirror/init(reflectingForTest:)-(Any)`` +/// - ``Swift/Mirror/init(reflectingForTest:)-(CustomTestReflectable)`` +/// +/// @Metadata { +/// @Available(Swift, introduced: 6.4) +/// } public protocol CustomTestReflectable { /// The custom mirror for this instance. /// /// Do not use this property directly. To get the test reflection of a value, - /// use ``Swift/Mirror/init(reflectingForTest:)``. + /// use ``Swift/Mirror/init(reflectingForTest:)-(CustomTestReflectable)``. + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.4) + /// } var customTestMirror: Mirror { get } } -@_spi(Experimental) +/// @Metadata { +/// @Available(Swift, introduced: 6.4) +/// } extension Mirror { /// Initialize this instance so that it can be presented in a test's output. /// @@ -33,6 +43,10 @@ extension Mirror { /// ## See Also /// /// - ``CustomTestReflectable`` + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.4) + /// } public init(reflectingForTest subject: some CustomTestReflectable) { self = subject.customTestMirror } @@ -45,6 +59,10 @@ extension Mirror { /// ## See Also /// /// - ``CustomTestReflectable`` + /// + /// @Metadata { + /// @Available(Swift, introduced: 6.4) + /// } public init(reflectingForTest subject: some Any) { if let subject = subject as? any CustomTestReflectable { self.init(reflectingForTest: subject) diff --git a/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift b/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift index 6f0517468..44f6539a2 100644 --- a/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift +++ b/Sources/Testing/SourceAttribution/CustomTestStringConvertible.swift @@ -11,69 +11,6 @@ /// A protocol describing types with a custom string representation when /// presented as part of a test's output. /// -/// Values whose types conform to this protocol use it to describe themselves -/// when they are present as part of the output of a test. For example, this -/// protocol affects the display of values that are passed as arguments to test -/// functions or that are elements of an expectation failure. -/// -/// By default, the testing library converts values to strings using -/// `String(describing:)`. The resulting string may be inappropriate for some -/// types and their values. If the type of the value is made to conform to -/// ``CustomTestStringConvertible``, then the value of its ``testDescription`` -/// property will be used instead. -/// -/// For example, consider the following type: -/// -/// ```swift -/// enum Food: CaseIterable { -/// case paella, oden, ragu -/// } -/// ``` -/// -/// If an array of cases from this enumeration is passed to a parameterized test -/// function: -/// -/// ```swift -/// @Test(arguments: Food.allCases) -/// func isDelicious(_ food: Food) { ... } -/// ``` -/// -/// Then the values in the array need to be presented in the test output, but -/// the default description of a value may not be adequately descriptive: -/// -/// ``` -/// ◇ Test case passing 1 argument food → .paella to isDelicious(_:) started. -/// ◇ Test case passing 1 argument food → .oden to isDelicious(_:) started. -/// ◇ Test case passing 1 argument food → .ragu to isDelicious(_:) started. -/// ``` -/// -/// By adopting ``CustomTestStringConvertible``, customized descriptions can be -/// included: -/// -/// ```swift -/// extension Food: CustomTestStringConvertible { -/// var testDescription: String { -/// switch self { -/// case .paella: -/// "paella valenciana" -/// case .oden: -/// "おでん" -/// case .ragu: -/// "ragù alla bolognese" -/// } -/// } -/// } -/// ``` -/// -/// The presentation of these values will then reflect the value of the -/// ``testDescription`` property: -/// -/// ``` -/// ◇ Test case passing 1 argument food → paella valenciana to isDelicious(_:) started. -/// ◇ Test case passing 1 argument food → おでん to isDelicious(_:) started. -/// ◇ Test case passing 1 argument food → ragù alla bolognese to isDelicious(_:) started. -/// ``` -/// /// ## See Also /// /// - ``Swift/String/init(describingForTest:)`` diff --git a/Sources/Testing/Testing.docc/Documentation.md b/Sources/Testing/Testing.docc/Documentation.md index fb4ecc347..667733a7d 100644 --- a/Sources/Testing/Testing.docc/Documentation.md +++ b/Sources/Testing/Testing.docc/Documentation.md @@ -68,6 +68,12 @@ their problems. - +### Value description and reflection + +- +- ``CustomTestReflectable`` +- ``CustomTestStringConvertible`` + ### Data collection - diff --git a/Sources/Testing/Testing.docc/Expectations.md b/Sources/Testing/Testing.docc/Expectations.md index 46b5553f6..43fb90c8d 100644 --- a/Sources/Testing/Testing.docc/Expectations.md +++ b/Sources/Testing/Testing.docc/Expectations.md @@ -91,7 +91,6 @@ the test when the code doesn't satisfy a requirement, use - ``Expectation`` - ``ExpectationFailedError`` -- ``CustomTestStringConvertible`` ### Representing source locations diff --git a/Sources/Testing/Testing.docc/describing-values.md b/Sources/Testing/Testing.docc/describing-values.md new file mode 100644 index 000000000..1a4988eca --- /dev/null +++ b/Sources/Testing/Testing.docc/describing-values.md @@ -0,0 +1,170 @@ +# Describing and reflecting values + + + +Add custom descriptions and mirrors to values you use in your tests. + +## Overview + +The testing library provides two protocols, ``CustomTestStringConvertible`` and +``CustomTestReflectable``, that you can use to customize the appearance of +values in Swift. The testing library uses these protocols to describe +parameterized test arguments and, if a call to ``expect(_:_:sourceLocation:)`` +or ``require(_:_:sourceLocation:)-5l63q`` fails, to describe any values you pass +to them. + +## Customize the description of a value + +You use the ``CustomTestStringConvertible`` protocol when you want to customize +the description of a value _during testing only_. Values whose types conform to +this protocol use it to describe themselves when the testing library presents +them as part of the output of a test. For example, this protocol affects the +display of values you pass as arguments to test functions or that are elements +of an expectation failure. + +By default, the testing library converts values to strings using +[`String(describing:)`](https://developer.apple.com/documentation/swift/string/init(describing:)-67ncf). +The resulting string may be inappropriate for some types and their values. If +you make the type of the value conform to ``CustomTestStringConvertible``, then +the testing library will use the value of its ``CustomTestStringConvertible/testDescription`` +property instead. + +For example, consider the following type: + +```swift +enum Food: CaseIterable { + case paella, oden, ragu +} +``` + +If you pass an array of cases from this enumeration to a parameterized test +function: + +```swift +@Test(arguments: Food.allCases) +func isDelicious(_ food: Food) { ... } +``` + +Then the testing library needs to present all elements in the array in its +output, but the default description of these values may not be adequately +descriptive: + +``` +◇ Test case passing 1 argument food → .paella to isDelicious(_:) started. +◇ Test case passing 1 argument food → .oden to isDelicious(_:) started. +◇ Test case passing 1 argument food → .ragu to isDelicious(_:) started. +``` + +When you adopt ``CustomTestStringConvertible``, you can include customized +descriptions in your test output instead. + +```swift +extension Food: CustomTestStringConvertible { + var testDescription: String { + switch self { + case .paella: + "paella valenciana" + case .oden: + "おでん" + case .ragu: + "ragù alla bolognese" + } + } +} +``` + +The testing library then uses ``CustomTestStringConvertible/testDescription`` to +present these values: + +``` +◇ Test case passing 1 argument food → paella valenciana to isDelicious(_:) started. +◇ Test case passing 1 argument food → おでん to isDelicious(_:) started. +◇ Test case passing 1 argument food → ragù alla bolognese to isDelicious(_:) started. +``` + +## Customize the reflection of a value + +When a call to ``expect(_:_:sourceLocation:)`` or to ``require(_:_:sourceLocation:)-5l63q`` +fails, the testing library presents the value or values you pass to these macros +in its output. + +The testing library uses [`Mirror.init(reflecting:)`](https://developer.apple.com/documentation/swift/mirror/init(reflecting:)) +to break down these values if they contain properties that may be of interest to +you. For instance, if the `isDelicious(_:)` test fails, you might see output +such as: + +``` +✘ Test isDelicious(_:) recorded an issue with 1 argument food → sandwich +↳ food.isDelicious → false +↳ food → sandwich +↳ sandwich → (toppings: [Food.pickles, Food.candyCorn]) +↳ toppings → [Food.pickles, Food.candyCorn] +↳ isDelicious → false +``` + +This output is expressive, but also contains redundant information. You can +refine it further by making `Food` conform to the ``CustomTestReflectable`` +protocol. + +```swift +extension Food: CustomTestReflectable { + var customTestMirror: Mirror { + switch self { + case let .sandwich(toppings): + let ingredientNames = toppings.map { String(describingForTest: $0) } + return Mirror( + self, + children: [(label: "toppings", value: ingredientNames)] + ) + default: + Mirror(self, children: []) + } + } +} +``` + +With this conformance, the output of the failed test is instead: + +``` +✘ Test isDelicious(_:) recorded an issue with 1 argument food → sandwich +↳ food.isDelicious → false +↳ food → sandwich +↳ toppings → ["pickles", "candy corn"] +↳ isDelicious → false +``` + +## Implement custom descriptions using private properties + +If part or all of your type's state is `private` or otherwise not visible to +your test target, you may not be able to implement ``CustomTestStringConvertible/testDescription`` +or ``CustomTestReflectable/customTestMirror`` in your test target. You can +implement these properties, without adding conformances to either protocol, in +your production target, and then add empty protocol conformances in your test +target. Make sure to use `internal` or `package` visibility for the properties +so that your test target is able to use them. + +```swift +// In your production target: + +extension Food { + package var testDescription: String { ... } + package var customTestMirror: Mirror { ... } +} + +// In your test target: + +import FoodTruck + +extension Food: CustomTestStringConvertible, CustomTestReflectable {} +``` + +- Note: If you use `internal` visibility for these properties, you must import + your production target into your test target using the `@testable` attribute. diff --git a/Tests/TestingTests/CustomTestReflectableTests.swift b/Tests/TestingTests/CustomTestReflectableTests.swift index 623c3364b..e547640a5 100644 --- a/Tests/TestingTests/CustomTestReflectableTests.swift +++ b/Tests/TestingTests/CustomTestReflectableTests.swift @@ -8,7 +8,7 @@ // See https://swift.org/CONTRIBUTORS.txt for Swift project authors // -@testable @_spi(Experimental) import Testing +@testable import Testing struct `CustomTestReflectable Tests` { @Test func `Can get a custom mirror from a value`() throws {