Refactoring Tests - Using #file and #line to indicate failure location

Episode #337 | 10 minutes | published on April 27, 2018 | Uses Xcode-9.3
Subscribers Only
When refactoring tests, you end up moving critical assertion logic outside of the test method. This can cause our tests to fail in the wrong spot. This becomes worse if multiple methods share test logic. When a test fails you want to know exactly where the failure occurred. By leveraging #file and #line expression literals we can move the failure back to where it should be, within the test method. We will also see how we can continue to use expectations outside of a test instance.

Extracting common test logic

Since most of our tests are performing the same operations (set up an expectation, wait for the response to come back, etc) we have a lot of duplication in our tests.

We can extract this code into a new home:

extension CryptoCompareClient {
    func testFetchCoinListVerifyingResponse(resultBlock: @escaping (ApiResult<CoinList>) -> Void) {

        let exp = XCTestExpectation(description: "Received coin list response")
        fetchCoinList { result in
            exp.fulfill()
            resultBlock(result)
        }

        let result = XCTWaiter.wait(for: [exp], timeout: 3.0)
        switch result {
        case .timedOut:
            XCTFail("Timed out waiting for coin list response")
        default:
            break
        }
    }    
}

Note the use of the XCTestExpectation and XCTWaiter classes, which are now required because we are no longer in a test instance.

This can help clean up our tests, but there's a problem with this:

Failures will be reported in the helper method, not the failing test.

This is an issue, especially if this method is reused for multiple tests.

In order to zero in to the actual failing test, we need to carry the failure back to the place where we called that method.

That's where some helpful expression literals come in handy.

Introducing #file and #line

The Swift compiler gives us a handy automatic expansion of two important pieces of data:

  • The current file
  • The current line number

We can pass this along to XCT assertion methods and this will influence where the error is reported.

 func testFetchCoinListVerifyingResponse(file: StaticString = #file, line: UInt = #line, resultBlock: @escaping (ApiResult<CoinList>) -> Void) {
   //...
}

Note the two additional arguments to this method. They have default values of #file and #line respectively. This means they will be automatically populated with values at the call site.

We then update our assertions:

XCTFail("Timed out waiting for coin list response", file: file, line: line)

And now when our tests fail, they fail in the test, and not in a helper.

blog comments powered by Disqus