Contents

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>

About the Author

Jon Reid is the author of iOS Unit Testing by Example. Find more at Quality Coding.

[[Bluesky]](https://bsky.app/profile/qualitycoding.org) [[Mastodon]](https://iosdev.space/@qcoding) [[YouTube]](https://www.youtube.com/@QualityCoding) [[GitHub]](https://github.com/jonreid)

Package Metadata

Repository: jonreid/failkit

Default branch: main

README: README.md