XCTest Error Handling Improvements

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:

Xcode test source code showing skipped test

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:

Xcode Test Report showing skipped test

See Also