Houman
Houman

Reputation: 66380

How can I fail an XCTest if a closure isn't invoked?

I have the following unit test:

let apiService = FreeApiService(httpClient: httpClient)
apiService.connect() {(status, error) in
     XCTAssertTrue(status)
     XCTAssertNil(error)
}

The actual function looks like this:

typealias freeConnectCompleteClosure = ( _ status: Bool, _ error: ApiServiceError?)->Void

class FreeApiService : FreeApiServiceProtocol {
    func connect(complete: @escaping freeConnectCompleteClosure) {
       ...
       case 200:
            print("200 OK")
            complete(true, nil)
    }
}

This unit test is passing, but the problem is, if I forget the complete(true, nil) part, the test will still pass. I can't find a way to make my test fail by default if the closure isn't called.

What am I missing?

Upvotes: 4

Views: 2351

Answers (2)

Jon Reid
Jon Reid

Reputation: 20980

It looks like you are trying to write a test against FreeApiService. However, judging from the name (and the completion handler), FreeApiService makes a network call. This is something to avoid in unit tests, because the test no longer depends on just your code. It depends on

  • Having a reliable Internet connection
  • The back end being up
  • The back end responding before a given time-out
  • The back end issuing the expected response

If you're willing to endure tests that are flaky (due to the dependencies above) and slow (due to network latency), then you can write an asynchronous test. It would look something like this:

func testConnect_ShouldCallCompletionHandlerWithTrueStatusAndNilError() {
    let apiService = FreeApiService(httpClient: httpClient)
    var capturedStatus: Bool?
    var capturedError: Error?

    let promise = expectation(description: "Completion handler invoked")
    apiService.connect() {(status, error) in
        capturedStatus = status
        capturedError = error
        promise.fulfill()
    }
    waitForExpectations(timeout: 5, handler: nil)

    XCTAssertTrue(capturedStatus ?? false, "status")
    XCTAssertNil(capturedError, "error")
}

If the completion handler isn't called, waitForExpectations will time out after 5 seconds. The test will fail as a result.

I avoid putting assertions inside completion handlers. Instead, as I describe in A Design Pattern for Tests that Do Real Networking, I recommend:

  • Capture arguments we want to test
  • Trigger the escape flag

All assertions can then be performed outside of the completion handler.

…But! I try to reserve asynchronous tests for acceptance tests of the back-end system itself. If you want to test your own code, the messy nature of asynchronous tests suggests that the design of the code under test is calling for improvements. A network call forms a clear boundary. We can test up to that boundary. And we can test anything that gets tossed back at us, but on our side of the boundary. In other words, we can test:

  • Is the network request formed correctly? But don't issue the request.
  • Are we handling network responses correctly? But simulate the responses.

Reshaping the code to allow these tests would let us write tests that are fast and deterministic. They don't even need an Internet connection.

Upvotes: 3

jscs
jscs

Reputation: 64012

You need to use an XCTTestExpectation for this. That will fail the test unless it is explicitly fulfilled.

Create the expectation as part of your test method setup by calling expectation(description:) or one of the related methods. Inside the callback in your test, invoke fulfill() on the expectation. Finally, after your call to connect(), call one of XCTestCase's "wait" methods, such as waitForExpectations(timeout:). If you forget to invoke the callback in your app code, the timeout will elapse and you will no longer have a false positive.

A full example is given in Apple's "Testing Asynchronous Operations with Expectations" doc.

Upvotes: 4

Related Questions