Upcoming and OnDemand Webinars View full list

Creating a Custom XCTest Assertion

Jeremy W. Sherman

Apple’s bundled test framework XCTest provides a very limited, general collection of assertions. These get the job done much of the time, but sometimes they’re not the right tool to communicate what you’re actually checking. That’s when it’s useful to know how to write custom assertions that clearly express what you are truly checking for without getting lost in a maze of little this-that-and-that assertions.

Noticing the Need

There are some signs it’s time to introduce your own, custom assertion:

  • There’s repeated code: “Why do all these tests end with the same three assertions?”
  • There’s too much code: “Why is the last half of this test method the gory details of marshaling and inspecting data? What does all this code even do?”
  • There’s code at the wrong level of abstraction: “Wait, I thought we were talking about people, but suddenly we’re talking about strings and numbers and date math and… what?”

Let’s go over those in slightly more detail.

Repeated Code

  • You find yourself copy-pasting and tweaking the target of some assertions
    across several tests. “And then those things should be equal. And also here. And here.”
  • You find yourself writing a sequence of assertions to check bits and
    pieces of two different object graphs. “Well, their names should be the same. Oh, and their ages. And they should have the same parents.”

Distracting Assertion

  • The amount of assertion code dwarfs all other code in the test.
  • You want to provide good feedback on test failure, but that means writing a good chunk of code,
    and all that code distracts from the actual focus of the test.
  • This is basically saying “Hey, we have another method’s body hanging out at the end of the test.”

Unclear Assertion

  • The lead-up to the assertion is more meaningful than the assertion
    itself: You’re pulling some data out and maybe proactively fail the test
    if it’s not there.
  • The assertion is hard to read or even misleading: (Double-negatives, anyone?)

Creating a Custom XCTest Assertion

Basically, you “just” extract method.

In practice, you need to be aware of a few tricks if you want to provide the same experience you’d get with the baked-in assertions.

There’s just enough to worry about that it’s a pain, so I’ll give you a snippet you can add to Xcode to make this easier at the end.

Write Some Tests

Say you have a test like so:

class MessageTests: XCTestCase {
    let sender: Messager = 
    var message: Message!

    override func setUp() { message =  }

    func testResendingOnlyUpdatesSentTime() {
        let resentMessage = message.resend()
        XCTAssert(message.sent.earlierDate(resentMessage.sent) == message.sent,
            "failed to make resent time later: was (message.sent), "
            + "resending made it (resentMessage.sent)")
        XCTAssertEqual(message.id, resentMessage.id, "id")
        XCTAssertEqual(message.text, resentMessage.text, "text")
        XCTAssertEqual(message.sender.name, resentMessage.sender.name,
            "sender name")
        XCTAssertEqual(message.sender.host, resentMessage.sender.host,
            "sender host")
    }
}

Notice the oodles of assertions. This is distracting!

But also notice what they’re really doing: providing field-by-field granularity for equality checking.

Even if the Message class were to implement Equatable and we were simply checking all fields were equal, we still might want a helper assertion that checks each field separately to simplify debugging by providing more granular diagnostics.

This is especially valuable if you have a large bundle of data where figuring out what values are not equal by visual inspection can be difficult, because you’re looking for a needle in a haystack. It’s a lot more helpful for a failing test to report, “Hey, this one field was wrong,” than, “One of these fifty fields is wrong, and some of them are collections. Good luck!”

Extract Method

Let’s grab that sequence of assertions and yank them out into their own method.

For want of a more clever name, let’s call the fields that don’t change on resend “durable fields”, and call our assertion AssertDurableFieldsEqual.

Behold! The power of extract method:

    func testResendingOnlyUpdatesSentTime() {
        let resentMessage = message.resend()
        XCTAssert(message.sent.earlierDate(resentMessage.sent) == message.sent,
            "failed to make resent time later: was (message.sent), "
                + "resending made it (resentMessage.sent)")
        AssertDurableFieldsEqual(message, resentMessage)
    }
}


func AssertDurableFieldsEqual(
    message: Message, _ otherMessage: Message
) {
    XCTAssertEqual(message.id, otherMessage.id, "id")
    XCTAssertEqual(message.text, otherMessage.text, "text")
    XCTAssertEqual(message.sender.name, otherMessage.sender.name, "sender name")
    XCTAssertEqual(message.sender.host, otherMessage.sender.host, "sender host")
}

Capture the Calling File and Line Number

This has one problem: Any failing assertions in AssertDurableFieldsEqual will log a failure against the line in that assertion method rather than in the test that’s using our assertion method. This makes it a lot harder to pinpoint what’s failing, especially if you have multiple tests using the custom assertion with multiple failures, because that leads to errors stacking up, and Xcode’s UI for that is not fun.

It turns out the file and line number blaming is done through default arguments. For example, have a look at the definition of XCTFail, which you’d call like XCTFail("should never be executed!"):

public func XCTFail(
    message: String = default,
    file: String = default,
    line: UInt = default
)

Those defaults turn out to be the compiler-provided identifiers #file and #line. Let’s add those arguments and defaults to our custom assertion so they get captured in the test method calling our assertion, at the call site:

func AssertDurableFieldsEqual(
    message: Message, _ otherMessage: Message,
    file: StaticString = #file, line: UInt = #line
) {
    XCTAssertEqual(message.id, otherMessage.id, "id")
    XCTAssertEqual(message.text, otherMessage.text, "text")
    XCTAssertEqual(message.sender.name, otherMessage.sender.name, "sender name")
    XCTAssertEqual(message.sender.host, otherMessage.sender.host, "sender host")
}

Take care to note that line’s type should be UInt, not the more common Int. You won’t run into any problems just yet, but you’ll get yelled at by the compiler if you use Int instead of UInt once you do the next, and final, step.

Pipe File and Line Number into All Assertions

There’s still one more step to get proper blaming on failure: We also need our custom assert method to tell all the assert methods it uses what file and line number should take the blame for failure.

We do this by explicitly providing the file and line arguments
to all assertions rather than allowing them to default to the file and line in our custom assertion method itself:

func AssertDurableFieldsEqualPipeFileAndLine(
    message: Message, _ otherMessage: Message,
    file: StaticString = #file, line: UInt = #line
) {
    XCTAssertEqual(message.id, otherMessage.id, "id",
        file: file, line: line)
    XCTAssertEqual(message.text, otherMessage.text, "text",
        file: file, line: line)
    XCTAssertEqual(message.sender.name, otherMessage.sender.name, "sender name",
        file: file, line: line)
    XCTAssertEqual(message.sender.host, otherMessage.sender.host, "sender host",
        file: file, line: line)
}

And that’s it! It’s kind of obnoxiously boiler-plate-y, but we now have an assertion that acts just like any baked-in XCTest assertion.

A Snippet

I have this snippet in my Xcode to simplify a few of the less-obvious bits that I might forget. I bet you’ll find it useful as well:

    func Assert<#Something#>(
        <#arg#>,
        file: StaticString = #file, line: UInt = #line
    ) {
        XCTAssertTrue(true, file: file, line: line)
    }

The “assert true” bit is there to remind me to pipe in the file and line. The rest is there so I don’t have to type out the file and line args and their defaults, but can get right to the part I care about.

I have it saved with completion shortcut jwsassert. Using your initials as a prefix for your custom snippets gives you autocomplete lookup of all your snippets just by typing a few characters.

In Conclusion

  • Custom assertions can DRY up your tests and make them clearer and easier to read. They can also simplify writing future tests by creating a testing vocabulary suited to your specific project and the objects in it.
  • Creating a custom assertion with XCTest is fundamentally an extract method refactoring, but you also need to take care to pipe the calling file and line number through to all assertions used within your new assert method so that Xcode reports the error against the line in the test method rather than reporting it against the line in your custom assertion method.
  • Add an Xcode snippet, and most of the obnoxious bits will be taken care of for you automatically (or at least with some , file: file, line: line) copy-pasting).

Happy testing!

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project