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()
}
}