jonreid/failkit
Writing custom test assertions makes your tests more expressive and easier to maintain.
Contents
Usage Fail.fail Better Value Descriptions with describe Add a Distinguishing Message Test Your Custom Assertions ✅ Success Case (No Failure) ❌ Failure Case (Should Fail) describe Details 💡 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 without Optional(…); strings quoted and escaped.
- Assertion Testing:
Use FailSpy to 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:
<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>
And in your target:
<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>
Package Metadata
Repository: jonreid/failkit
Default branch: main
README: README.md