.st0{fill:#FFFFFF;}

How to Make Custom Test Assertions in Swift 

 July 26, 2016

by  Jon Reid

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.

By “location of the failure,” I mean the file name and line number of the code that calls the assertion.

#filePath and #line

Swift directly incorporates these important values into the language as “literal expressions.” They are #filePath and #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:

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

There are 3 parameters: a message, a file name, and a line number. We usually call this with the message alone, not specifying the last two parameters. Their default values are #filePath 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.

Motivating Example: Testing Tuples

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:

XCTAssertEqual(pair, (3, 11))

But this doesn’t work because tuples aren’t Equatable. OK, then how about this?

XCTAssertTrue(pair == (3,11))

This one does work because there are generic operators for 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, time is wasted. Why can’t we get this information the first time around?

Every time you have to fire up the debugger to analyze a test failure, time is wasted.

Click to Tweet

The XCTest assertions all have an optional message parameter. So we can use this to report the actual value:

let pair = (1, 2)
XCTAssert(pair == (3, 11), "was \(pair)")

This gives us the following message:

XCTAssertTrue failed - was (1, 2)

That’s 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”…

Writing a Custom Test Assertion

Let's use what we learned about #filePath and #line. We can start writing a function to check pairs of integers:

func assertIntPairsEqual(
    actual: (_: Int, _: Int),
    expected: (_: Int, _: Int),
    file: StaticString = #filePath,
    line: UInt = #line
)

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 let’s specify the optional message while we’re at it:

func assertIntPairsEqual(
    actual: (_: Int, _: Int),
    expected: (_: Int, _: Int),
    file: StaticString = #filePath,
    line: UInt = #line
) {
    if actual != expected {
        XCTFail("Expected \(expected), but was \(actual)", file: file, line: line)
    }
}

Now let’s change our test code to use this new assertion.

let pair = (1, 2)
assertIntPairsEqual(actual: pair, expected: (3, 11))

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:

func assertPairsEqual<T: Equatable, U: Equatable>(
    actual: (_: T, _: U),
    expected: (_: T, _: U),
    file: StaticString = #filePath,
    line: UInt = #line
)

Conclusion

Here are the steps for creating specialized test assertions in Swift:

  • Define your assertion as a helper function.
  • Design the parameters to be unambiguous.
  • Include optional parameters for file and line.
  • Upon failure, call XCTFail, passing the file and line arguments.
  • Report all the information you need to diagnose failures.
  • Can you make the assertion generic?

The Swift implementation of XCTest makes this easy.

Have you written any custom test assertions in Swift? Please describe them below!

The Definitive Guide from Pragmatic Programmers

My book iOS Unit Testing by Example: XCTest Tips and Techniques Using Swift is the definitive guide to unit testing iOS apps. It covers foundational tools and skills, testing specific behaviors of iOS apps, and how to use the fast feedback from your tests.

Jon Reid

About the author

Programming was fun when I was a kid. But working in Silicon Valley, I saw poor code lead to fear, with real human costs. Looking for ways to make my life better, I learned about Extreme Programming, including unit testing, test-driven development (TDD), and refactoring. Programming became fun again! I've now been doing TDD in Apple environments for 20 years. I'm committed to software crafting as a discipline, hoping we can all reach greater effectiveness and joy. Now a coach with Industrial Logic!

  • I like this technique and would like to add that with Swift you can even define a func inside another func. For example when you want to do multiple similar assertions during one test.

    Example:

    <span class="keyword">func</span> testReset() {
      <span class="keyword">func</span> assertInDefaultState(foo: <span class="type">Foo</span>, line: <span class="type">UInt</span> = <span class="keyword">#line</span>) {
        <span class="call">XCTAssertTrue</span>(foo.<span class="property">bar</span>, line: line)
        <span class="call">XCTAssertFalse</span>(foo.<span class="property">foo</span>, line: line)
        <span class="comment">// ...</span>
      }
      <span class="keyword">let</span> foo = <span class="type">Foo</span>()
      <span class="call">assertInDefaultState</span>(foo)
      foo.<span class="property">bar</span> = <span class="keyword">false</span>
      <span class="comment">// more modifications and possibly assertions</span>
      foo.<span class="call">reset</span>()
      <span class="call">assertInDefaultState</span>(foo)
    }

    One more example I’d like to share is how I assert that two NSIndexPaths are the same.

    <span class="keyword">func</span> assertEqualIndexPaths(first: <span class="type">NSIndexPath</span>, <span class="keyword">_</span> second: <span class="type">NSIndexPath</span>, line: <span class="type">UInt</span> = <span class="keyword">#line</span>) {
      <span class="keyword">let</span> a = (section: first.<span class="property">section</span>, row: first.<span class="property">row</span>)
      <span class="keyword">let</span> b = (section: second.<span class="property">section</span>, row: second.<span class="property">row</span>)
      <span class="call">XCTAssertTrue</span>(a == b, <span class="string">"</span>\(a) <span class="string">!=</span> \(b)<span class="string">"</span>, line: line)
    }

    I use it in one place in one file, so I don’t use #file here. It’s actually also just a function defined inside test function.

    Hope there ideas will help someone :)

  • Better late than never, I just found your excellent article
    and used your ideas (still valid in Swift 5.2) to write assertMatches below. Thanks!

    <span class="comment">// example 1 - pass</span>
            <span class="call">assertMatches</span>(<span class="string">"(Date())"</span>, <span class="string">#"^d{4}-d{2}-d{2} d{2}:d{2}:d{2} +d{4}$"#)</span>
    <span class="comment">// example 2 - fail ...</span>
            <span class="call">assertMatches</span>(<span class="string">"(Date())"</span>, <span class="string">#"^d{4}-d{2}-d{2} d{2}:d{2}:d{2} +d{4}===$"#)</span>
    <span class="comment">// ... with message</span>
    failed - match '<span class="number">2020</span>-<span class="number">03</span>-<span class="number">20 21</span>:<span class="number">17</span>:<span class="number">57</span> +<span class="number">0000</span>' against pattern '^d{<span class="number">4</span>}-d{<span class="number">2</span>}-d{<span class="number">2</span>} d{<span class="number">2</span>}:d{<span class="number">2</span>}:d{<span class="number">2</span>} +d{<span class="number">4</span>}===$'
    
    <span class="comment">// code</span>
    <span class="keyword">func</span> assertMatches(
        <span class="keyword">_</span> actual: <span class="type">String</span>,
        <span class="keyword">_</span> pattern: <span class="type">String</span>,
        file: <span class="type">StaticString</span> = <span class="keyword">#file</span>, line: <span class="type">UInt</span> = <span class="keyword">#line</span>
    ) {
        <span class="keyword">if</span> !(actual ~= pattern) {
            <span class="type">XCTFail</span>(<span class="string">"match '(actual)' against pattern '(pattern)'"</span>,
                    file: file, line: line)
        }
    }
    <span class="comment">/**
     Originally from
     https://www.hackingwithswift.com/articles/108/how-to-use-regular-expressions-in-swift
     */</span>
    <span class="keyword">extension</span> <span class="type">NSRegularExpression</span> {
        <span class="keyword">convenience init</span>(<span class="keyword">_</span> pattern: <span class="type">String</span>) {
            <span class="keyword">do</span> {
                <span class="keyword">try self</span>.<span class="keyword">init</span>(pattern: pattern)
            } <span class="keyword">catch</span> {
                <span class="call">preconditionFailure</span>(<span class="string">"Illegal regular expression: (pattern)."</span>)
            }
        }
        <span class="keyword">func</span> matches(<span class="keyword">_</span> string: <span class="type">String</span>) -> <span class="type">Bool</span> {
            <span class="keyword">let</span> range = <span class="type">NSRange</span>(location: <span class="number">0</span>, length: string.<span class="property">utf16</span>.<span class="property">count</span>)
            <span class="keyword">return</span> <span class="call">firstMatch</span>(in: string, options: [], range: range) != <span class="keyword">nil</span>
        }
    }
    <span class="keyword">func</span> ~= (lhs: <span class="type">String</span>, rhs: <span class="type">String</span>) -> <span class="type">Bool</span> {
        <span class="keyword">guard let</span> regex = <span class="keyword">try</span>? <span class="type">NSRegularExpression</span>(pattern: rhs) <span class="keyword">else</span> { <span class="keyword">return false</span> }
        <span class="keyword">let</span> range = <span class="type">NSRange</span>(location: <span class="number">0</span>, length: lhs.<span class="property">utf16</span>.<span class="property">count</span>)
        <span class="keyword">return</span> regex.<span class="call">firstMatch</span>(in: lhs, options: [], range: range) != <span class="keyword">nil</span>
    }
  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}

    Never miss a good story!

    Want to make sure you get notified when I release my next article or video? Then sign up here to subscribe to my newsletter. Plus, you’ll get access to the test-oriented code snippets I use every day!

    >