Writing custom test assertions makes your tests more expressive and easier to maintain.
But how do you support both XCTest and Swift Testing?
XCTest uses XCTFail. Swift Testing uses Issue.record. You can’t just call one from the other. You could write your assertions twice — or use FailKit.
-
Unified Failure Reporting:
Works with XCTest and Swift Testing, including source location. -
Cleaner Value Descriptions:
Optional values withoutOptional(…); strings quoted and escaped. -
Assertion Testing:
UseFailSpyto test your custom assertions: did they fail, and how?
Let’s say we want a custom equality assertion that’s clearer than XCTestAssertEqual:
func assertEqual<T: Equatable>(
_ actual: T,
expected: T,
file: StaticString = #filePath,
line: UInt = #line
) {
if actual == expected { return }
XCTFail("Expected \(expected), but was \(actual)", file: file, line: line)
}This works — until you start migrating to Swift Testing. You’ll need to duplicate the function, rename it, and re-implement the failure logic.
With FailKit, you can write one assertion that works in both worlds:
func assertEqual<T: Equatable>(
_ actual: T,
expected: T,
fileID: String = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) {
if actual == expected { return }
Fail.fail(
message: "Expected \(expected), but was \(actual)",
location: SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column)
)
}Fail.fail automatically routes to the appropriate testing framework.
Consider this failure message:
"Expected \(expected), but was \(actual)"Depending on the type, the results may be unclear:
| Type | Without FailKit | With describe() |
|---|---|---|
| Int | Expected 123, but was 456 | Expected 123, but was 456 |
| Int? | Expected Optional(123), but was Optional(456) | Expected 123, but was 456 |
| String | Expected ab cd, but was de fg | Expected "ab cd", but was "de fg" |
Improve this by using:
"Expected \(describe(expected)), but was (describe(actual))"Optional values are unwrapped. Strings are quoted and escaped, making special characters visible.
When a test has multiple assertions, it helps to add a short distinguishing message:
let result = 6 * 9
assertEqual(result, 42, "answer to the ultimate question")To support this, add a message parameter with a default:
When a test has multiple assertions, it’s often helpful to add a distinguishing message. This helps us identify the point of failure even from raw console output, as you get from a build server.
To separate this distinguishing message from the main message, use FailKit’s messageSuffix function. First, add a String parameter with a default value of empty string:
func assertEqual<T: Equatable>(
_ actual: T,
expected: T,
message: String = "",
...
)And append it using messageSuffix:
"Expected \(expected), but was \(actual)" + messageSuffix(message)FailKit will insert a separator if the message is non-empty:
Expected 42, but was 54 - answer to the ultimate question
You can test your assertion helpers using FailSpy. First, modify your function to take a Failing parameter:
func assertEqual<T: Equatable>(
_ actual: T,
expected: T,
...,
failure: any Failing = Fail()
)Then, call failure.fail(…) instead of Fail.fail(…).
To test it:
@Test
func equal() async throws {
let failSpy = FailSpy()
assertEqual(1, expected: 1, failure: failSpy)
#expect(failSpy.callCount == 0)
}@Test
func mismatch() async throws {
let failSpy = FailSpy()
assertEqual(2, expected: 1, failure: failSpy)
#expect(failSpy.callCount == 1)
#expect(failSpy.messages.first == "Expected 1, but was 2")
}You can now test your own test helpers — and TDD them, too.
The describe() function formats values to improve test output:
- Optionals: Removes
Optional(…)wrapper - Strings: Wraps in quotes and escapes special characters
\"(quote)\n,\r,\t(newline, carriage return, tab)
- Other types: Use default Swift description
Check out the Demo folder to see:
- A real custom assertion built using FailKit
- How to test that assertion using
FailSpy
It’s a complete, working example you can use as a starting point for your own helpers.
Use Swift Package Manager:
dependencies: [
.package(url: "https://github.com/jonreid/FailKit.git", from: "1.0.0"),
],And in your target:
dependencies: ["FailKit"]Jon Reid is the author of iOS Unit Testing by Example.
Find more at Quality Coding.