The release of Xcode 11.4 has some interesting XCTest improvements that make it a little easier to write tests that handle throwing errors and conditionally skip tests.
What Changed in Xcode 11.4?
If you add a new UI or unit test case class to your project with Xcode 11.4 you may notice some changes:
override func setUpWithError() throws {}
override func tearDownWithError() throws {}
func testExample() throws {}
func testPerformanceExample() throws {}
Instead of the old setUp
and tearDown
methods you now have setUpWithError
and tearDownWithError
. The use of throwing test methods is not new in Xcode 11.4 but the template now marks both example test methods with throws
by default.
Note: The old methods are not deprecated. You can still use setup
and tearDown
and non-throwing test methods.
Throwing is a Test Failure
It’s not that difficult to test that an operation does not throw an error. Suppose I’m testing the import of some JSON data. If all I care about is that the operation does not throw an error I can use XCTAssertNoThrow
:
func testNoThrowImport() {
XCTAssertNoThrow(try importJSON(name: "testData"))
}
When you want to do something more with the imported data you can wrap the throwing operation in a do
block and catch any errors:
func testNoThrowImport() {
do {
let countries = try importJSON(name: "testData")
XCTAssertTrue(countries.count == 10)
} catch {
XCTFail("Import fail: \(error)")
}
}
I find refactoring that into a throwing helper method more readable:
func testNoThrowImport() {
XCTAssertNoThrow(importFromJson)
}
func importFromJson() throws {
let countries = try importJSON(name: "testData")
XCTAssertTrue(countries.count == 10)
}
The only problem is that my test class becomes littered with these small throwing helper test methods. A better approach is to mark the test method as throwing and avoid the helper method or do
block. This achieves the same result in a more concise and readable way:
func testImportTenCountries() throws {
let countries = try importJSON(name: "test10")
XCTAssertTrue(countries.count == 10)
}
If the import throws an error Xcode records the test as failed.
Throwing During Setup
This also works with the setUpWithError
and tearDownWithError
methods. Suppose I’m importing my JSON data in the setup code. Using the throwing version:
override func setUpWithError() throws {
countries = try importJSON(name: "test10")
}
If the setup code throws an error the test fails without executing the test method(s).
Skipping Tests
One other test improvement that Apple snuck into Xcode 11.4 allows you to skip tests based on runtime conditions. For example, if I was retrieving test data from a QA server at runtime:
func testThatSkips() throws {
try XCTSkipUnless(qaServerReachable(), "QA server not reachable")
// Retrieve test data
// Perform test
}
If the qaServerReachable
method returns false
Xcode skips the rest of the test and it shows up with a gray symbol in the test navigator and test source code:
There’s also a XCTSkipIf
variant:
try XCTSkipIf(qaServerUnreachable(), "QA Server unreachable")
The Xcode test report shows skipped tests separately from successful or failed tests: