When a test fails, we want to know the location of the failure. Getting this information in Objective-C required us to dance with the preprocessor. But with Swift, it’s much more straightforward.
Objective-C is a language with two sides: the C side, and (practically speaking) the Smalltalk side. The C side not only gave us the C language, but the C preprocessor. The preprocessor is a way to manipulate the code you write into the code that is actually compiled. We used it all the time without giving it much thought. Every
#import statement is a preprocessor directive to insert text, and so on.
The preprocessor could do magic, for good or ill. I learned to avoid it as much as possible. But sometimes it was unavoidable. The predefined macros
__LINE__ expanded to the name of the current file, and the current line number. These give us the information we need to report test failures.
But some people assumed this meant you had to write preprocessor macros for everything that used this information. This led to the unfortunate pattern of Objective-C XCTest assertions written entirely as macros. (By contrast, my approach with OCHamcrest and OCMockito was to get the file name and line number and get out. That is, I passed them as arguments to ordinary functions and methods.)
Swift directly incorporates these important values into the language as “literal expressions”. They are
#line. You can use them anywhere, but they’re particularly useful as default parameter values.
Do you know the assertion XCTFail? That’s the one that will always report a failure. Let’s look at its signature:
There are 3 parameters: an optional message, an optional file name, and an optional line number. We usually call this without specifying the last two parameters. Their default values are #file and #line, which evaluate to their position at the point of call. This is important. We don’t get the location of the function. Instead, we get the location of the place that calls the function.
Let’s say you want a test that confirms the values of a tuple. We’ll use a pair of integers for simplicity. How would you write a test to check its values? I’d first try this:
But this doesn’t work because tuples are currently not
Equatable. OK, then how about this?
That works in Swift 2.2 and greater. (There are generic operator functions that work on pairs and other tuples.) But consider what it reports upon failure:
XCTAssertTrue failed –
All it tells us is that the assertion failed. It doesn’t tell us why. Every time you have to fire up the debugger to analyze a test failure is time wasted. Why can’t we get this information the first time around?
The XCTest assertions all have an optional message parameter. So we can use this to report the actual value:
This gives us the following message:
XCTAssertTrue failed – was (1, 2)
That’s much better! But having to repeat this code for every pair we test… that’s duplicate code, crying out for refactoring. Time to apply “Extract Method”…
Let’s use what we learned about #file and #line. We can start writing a function to check pairs of integers:
Now we know that within this function, the file and line parameters will reflect the location of the point of call.
Writing the comparison is easy. What do we do if it fails? We want to report it to XCTest.
Here’s the trick: Remember XCTFail? It has those optional parameters for file and line. What if we don’t use their default values, but specify our own? And the optional message while we’re at it:
This produces the following message:
failed – Expected (3, 11) but was (1, 2)
There we go! To top it off, we can make it more generic. The following assertion can handle any pairs of any
Equatable types T and U:
Here are the steps for creating specialized test assertions in Swift:
Swift makes this aspect of testing easy! (But I expect to hit the problems soon…)
Have you written any custom test assertions in Swift? Please describe them below!
Jon is a coach and consultant on iOS Clean Code (Test Driven Development, unit testing, refactoring, design). He’s been practicing TDD since 2001. You can learn more about his background, or see what services he can bring to your organization.