Last Updated: 2018-10-07

Using Swift #file and #line Literals for Custom XCTest Assertions

I often forget the "right" way to do custom xctest assertions by using the #file and #line literals, and wanted to have a quick reference for myself.

If you're not already familiar, see #file and #line are constants that are generated at runtime and are captured at the call site, rather than where a function or method is defined.

We can break that down a bit more by looking at the implementation of the various XCTAssert functions. You'll find that #file and #line very important to how assertions are able to provide feedback as to the location of the assertion that has failed within your test suite:

public func XCTFail(_ message: String = "", file: StaticString = #file, line: UInt = #line)

When you call XCTFail the file and line of the call site are used, instead of the file and line where XCTFail is defined.

It's potentially worth examining the internals of XCTAssertTrue if you haven't, as there are some interesting implementation details regarding how exceptions are handled, but to keep it short:

XCTAssertTrue (and similar) provide special handling for exceptions so that the entire test suite doesn't crash on unexpected exceptions.

If you really want to dig in deeper, check out the implementation on Github

All that this means is that our custom assertions should (in most cases) be composed of calls to either XCTAssertTrue or XCTAssertFalse.

Let's build an assertion that a checks if a string fits in to a certain range:

public func XCTAssertString(_ string: String,
                            fitsInRange range: ClosedRange<Int>,
                            file: StaticString = #file,
                            line: UInt = #line) {
    let actualLength = string.count
    XCTAssertTrue(range.contains(actualLength),
        """
        Expected string between \(range.lowerBound)
        and \(range.upperBound) characters, got \(actualLength)
        """,

        // We pass through our file and line so that the "real"
        // file and line where this is called from are
        // where any failures are recorded.
        file: file,
        line: line)
}

It's important to use #file and #line arguments in our implementation to capture those values from the call site, and equally important to pass our values through to XCTAssertTrue, otherwise failures will be captured wherever our function is defined, rather than where we call it.

You can try this out on your own with this sample code:

public func XCTAssertFailsInWrongPlace(file: StaticString = #file,
                                       line: UInt = #line) {
    // We'll see a failure here, or many many failures if we use this
    // assertion throughout our test suite.
    XCTFail("Forgot to pass through our file and line!")
}

class TestFailsInWrongPlace: XCTestCase {
    func testAssertionFailsInWrongPlace() {
        // The failure doesn't show up here, but instead where we defined our assertion!
        XCTAssertFailsInWrongPlace()
    }
}

There's more to read!

Did this help solve your problem? If so, consider letting us know on Twitter. You should follow us for all the latest articles and updates:


Have you seen our latest project on Github?.

Sign up for our newsletter

Get an email when new content comes out! Emails are not sold and are used only for announcing updates to the site. Expect ~1-2 emails per month. Unsubscribe any time.