Under the hood of Assertions in Swift

Assertions are not only an essential tool when writing tests - they're also super useful in order to write more predictable and easier to debug code.

Even though Swift has a heavy focus on using the type system to make code more reliable, we sometimes still face situations in which we can't be 100% sure that certain conditions have been met. In such situations a well-placed assert can really help when trying to figure out what the problem is.

Back in "Picking the right way of failing in Swift", we took a look at the various error handling mechanisms that we have at our disposal in Swift, and in which type of situations to apply them. This week, let's dive a bit deeper into assertions in particular, how they work and how we can implement our own assert() functions for performing various checks.

When to assert

In production code, assertions provide a way to trigger a non-recoverable error that only gets evaluated in debug builds. As such, they are perfect for telling the programmer - not the user - that something went wrong. Placing asserts in code paths that you really don't expect to get executed is a great way to identify logic problems and clear out bugs as early as possible.

For example, let's say that we have a UITableViewDataSource implementation that expects dequeued cells to be of a certain custom class. Rather than using force-casting and causing crashes, triggering an assertionFailure in case an incorrect cell type was encountered can help a fellow developer (which might be a future version of yourself πŸ˜…) understand what went wrong:

func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let anyCell = tableView.dequeueReusableCell(withIdentifier: "cell", 
                                                for: indexPath)

    guard let cell = anyCell as? MyCustomCell else {
        assertionFailure("Unexpected cell type: \(type(of: anyCell))")
        return anyCell
    }

    // Cell setup
    ...
}

Of course, the most common use of asserts is not in our production code - it's in our tests, where we use the XCTAssert family of assertion functions to verify that values and objects match our expectations:

func testAddingNumbers() {
    calculator.number = 5
    calculator.add(3)
    XCTAssertEqual(calculator.number, 8)
}

Under the hood

So how do asserts really work? Their implementation is actually quite simple, but they use some special compiler features that are really interesting to take a closer look at. Here's pretty much what the assert implementation looks like in the Swift standard library:

func assert(_ condition: @autoclosure () -> Bool,
            _ message: @autoclosure () -> String,
            file: StaticString = #file,
            line: UInt = #line) {
    // Don't evaluate in debug builds
    guard isDebugAssertConfiguration() else {
        return
    }

    // Execute the condition (false == failure)
    if !condition() {
        // Forward the call to the system API for reporting asserts & errors
        _assertionFailure("Assertion failed", message(), file: file, line: line)
    }
}

As you can see above, the function signature of assert uses some clever tricks in order to make the API easier to use.

First, it uses @autoclosure to avoid evaluating expressions in non-debug configurations. If you want to learn more about @autoclosure and how to create your own APIs using it - check out "Using @autoclosure when designing Swift APIs".

It also uses Swift's limited (but existing) pre-processing macros to have the compiler automatically fill in the file name and line number from which the function was called. Xcode even hides parameters using these type of macros (like #file, #line & #column) when autocompleting, making it a nice way to pass in this type of information without impacting API usage.

Now that we know how it works, let's build our own!

One of my favorite things about the Swift standard library, is that the techniques and compiler features that it uses are also available to anyone writing Swift programs. That means that we can actually write our own assert functions that uses the same signature style, but provide more specialized assertion features.

Let's say that we want to write a test to verify that one of our classes throws the correct error in case something went wrong. In order to keep our test code clean and easy to read, we'd like to be able to write something like this:

class FileLoaderTests {
    func testLoadingNonExistingFileThrows() {
        let loader = FileLoader()

        assert(loader.loadFile(at: "Not a file"),
               throwsError: FileLoader.Error.missingFile)
    }
}

That's easily achievable with a custom assert function, that we define very similarly to how it's done in the standard library:

public func assert<T, E: Error>(
    at file: StaticString = #file,
    line: UInt = #line,
    _ expression: @autoclosure () throws -> T,
    throwsError errorExpression: @autoclosure () -> E
) where E: Equatable {
    do {
        // Evaluate the expression, and throw away any result
        _ = try expression()

        // If execution continues, it means that the expression
        // didn't throw, which in this case is a failure
        XCTFail("Expected expression to throw", file: file, line: line)
    } catch let thrownError as E {
        let expectedError = errorExpression()

        XCTAssert(thrownError == expectedError,
                  "Incorrect error thrown. \(thrownError) is not equal to \(expectedError)",
                  file: file,
                  line: line)
    } catch {
        XCTFail("Invalid error thrown: \(error)", file: file, line: line)
    }
}

Using the above template, you can quickly define specialized assert functions - both for tests and for verifying conditions in production code. I've gathered up all of my custom test assert functions on GitHub here.

Conclusion

Assertions are conceptually quite simple, but can be really powerful when placed in the right spots. By understanding how they work under the hood, we can also easily create our own, specialized assert functions that can help us write cleaner and more expressive code.

I personally find that a good mix of tests and inline assertions can really make code easier to maintain over time, as the expected behaviors and caveats of each API become more clear, and any invalid use can quickly get caught during development.

What do you think? How do you use assertions in your projects - and do you find the ability to create your own useful? Let me know, along with any comments, questions or feedback that you might have - either here in the comments section below or on Twitter @johnsundell.

Thanks for reading! πŸš€

Creating custom collections in Swift

Using the factory pattern to avoid shared state in Swift