FailKit
Write expressive custom assertions that work on either Swift Testing or XCTest
Install / Use
/learn @jonreid/FailKitREADME
FailKit
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.
Contents
- Features
- Usage
describeDetails- 💡 See a Working Example
- Installation
- About the Author<!-- endToc -->
Features
-
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?
Usage
Fail.fail
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.
Better Value Descriptions with describe
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.
Add a Distinguishing Message
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
Test Your Custom Assertions
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:
✅ Success Case (No Failure)
@Test
func equal() async throws {
let failSpy = FailSpy()
assertEqual(1, expected: 1, failure: failSpy)
#expect(failSpy.callCount == 0)
}
❌ Failure Case (Should Fail)
@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.
describe Details
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
💡 See a Working Example
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.
Installation
Use Swift Package Manager:
<!-- snippet: dependency-declaration --><a id='snippet-dependency-declaration'></a>
dependencies: [
.package(url: "https://github.com/jonreid/FailKit.git", from: "1.0.0"),
],
<sup><a href='/Demo/Package.swift#L13-L17' title='Snippet source file'>snippet source</a> | <a href='#snippet-dependency-declaration' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->And in your target:
<!-- snippet: dependency-use --><a id='snippet-dependency-use'></a>
dependencies: ["FailKit"]
<sup><a href='/Demo/Package.swift#L21-L23' title='Snippet source file'>snippet source</a> | <a href='#snippet-dependency-use' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->About the Author
Jon Reid is the author of iOS Unit Testing by Example.
Find more at Quality Coding.
