Give a big hi to Tim V! He’ll be posting here when a topic inspires him and today, he’s going to talk about how to write tests that fail gracefully.
Most people that have been writing Swift code for a while try to limit their usage of optional force unwraps and try!
as much as possible. Test code, on the other hand, is often still littered with unsafe code. It’s true that crashes in tests aren’t nearly as undesirable as in production code, but it’s fairly straight-forward to write tests that fail gracefully when an unexpected nil
is encountered, or when an error is thrown unexpectedly.
Consider the following unit tests:
class Tests: XCTestCase { func testFoo() { let value = try! foo() XCTAssertEqual(value, 5) } func testBar() { let value = bar()! XCTAssertEqual(value, "bar") } }
What a lot of people don’t seem to know is that individual tests can be marked with throws
to automatically handle thrown errors:
func testFoo() throws { let value = try foo() XCTAssertEqual(value, 5) }
Much better. To write testBar
in a safer way, we’ll need to throw an error when the output of bar
is nil
. We could declare a separate error for each optional value we want to unwrap in one of our tests, but that requires writing a lot of extra code. Instead, we can throw a more general Optional.Error.unexpectedNil
each time an unexpected nil
value is encountered:
extension Optional { enum Error: Swift.Error { case unexpectedNil } func unwrap() throws -> Wrapped { guard let value = self else { throw Error.unexpectedNil } return value } }
Note: Swift 3.0 and below does not support nesting types inside a generic type, so if you’re not yet using Swift 3.1, you’ll have to declare a separate enum OptionalError
instead.
Now we can rewrite testBar
as follows:
func testBar() throws { let value = try bar().unwrap() XCTAssertEqual(value, "bar") }
After these changes, whenever a test would normally crash, it now simply fails. And as a bonus, all tests are guaranteed to be executed, where previously a single crash would prevent the remaining tests from being run.
3 Comments
Big hi! Of course, when a test fails gracefully, the cascading failures are, in themselves, a joy to behold!
`fund unwrap(withErrorMessage: String) throws` would be better. With plain `unwrap()`, if something fails you won’t get a useful error message.
My colleague Jeremy points out this is like XCTAssertFalse, which is similarly useless in test logs when it fails. You need to provide a message to make the logs helpful.
My main purpose was to get rid of force unwraps inside tests with the least possible effort, and a failed force unwrap gives no additional info either. But if getting a descriptive error message is something you care about, then adding an optional parameter to the unwrap method is a great idea!