Friday, February 24, 2017

Let Your Swift XCTest Methods Throw

Brian King:

One place where the XCTest assertion utilities fall a bit short has been with managing Optional variables in Swift. XCTAssertNotNil doesn’t provide any mechanism for unwrapping variables, easily leading to assertion checks like this[…]

[…]

A nice solution is possible, due to an often-overlooked feature of XCTestCase. If the test function is marked with throws, any thrown exception will cause the test to fail. We can use this to fail our tests using normal Swift flow control mechanisms[…]

Unit tests are much more pleasant to read and write if you can get rid of the clutter. There are two key ways that I do this. First, I have an MJTTestCase subclass with convenience methods that wrap the XCTAssert functions. My methods have obscenely short names. For example, XCTAssertTrue() becomes t() and XCTAssertEqual() becomes eq(). Swift’s support for unnamed parameters and overrides really helps here. These are not features that I use much in regular code, but they really come in handy with tests, where there are a small number of methods that are called many times, and I want the focus to be on the parameters rather than the methods themselves.

Second, as King describes, I take advantage of the fact that test methods are now allowed to throw. This is so much better than force unwrapping. My equivalent to his AssertNotNilAndUnwrap() is called unwrap(). It avoids having to write lots of guard statements, either returning the value in the optional or failing the test. If the test fails, it throws, which is how the return type can be T instead of T?. I also have variants like unwrapString(), which also do an as? to check the type.

The same technique works for checking errors. I have ok(), which takes an expression that can throw and fails the test (collecting the line number and error) if it does. If it succeeds, the return value is available for use. I also have e(), which makes sure that an NSError was thrown and returns it so that it can be inspected with further assertions. The XCTest equivalent is XCTAssertThrowsError(), which wants you to pass in an error handler closure. The closure has a number of drawbacks: it causes extra boilerplate and indentation, the closure’s body doesn’t auto-indent properly due to an Xcode bug, and my subclass convenience methods must be accessed through self. Instead, I can simply write:

let error = try e(codeThatShouldThrow())
eq(error.code, NSFileNoSuchFileError)

Previously: Proposal: XCTest Support for Swift Error Handling.

Update (2017-03-05): By request, here’s the source for my unwrap():

func unwrap<T>(_ value: T?, file: StaticString = #file, line: UInt = #line) throws -> T {
    guard let value = value else {
        fail("Unwrapped nil instead of \(T.self)", file: file, line: line)
        throw ExpectedNotNilError()
    }
    return value
}

Update (2019-10-13): Xcode 11 now includes its own XCTUnwrap (via Bas Broek).

2 Comments RSS · Twitter

[…] Let Your Swift XCTest Methods Throw […]

Leave a Comment