OHHTTPStubs and asynchronous tests

AliSoftware edited this page Aug 19, 2016 · 13 revisions

Asynchronous Testing

As OHHTTPStubs is a library generally used in Unit Tests invoking network requests, those tests are generally asynchronous. This means that the network requests are generally sent asynchronously, in a separate thread or queue different from the thread used to execute the test case.

As a consequence, you will have to ensure that your Unit Tests wait for the requests to finish (and have their response arrived) before performing the assertions in your Test Cases, because otherwise your code making the assertions will execute before the request had time to get its response.

Bad solution: when you don't wait for the async operation

For example, this won't work, because sendAsynchronousRequest:queue:completionHandler: will return immediately (triggering the networking request in the background) and thus the testFoo method will reach its end before the completionHandler block had time to be called.

So your test framework will see that the test didn't trigger any assertion failure and will mark the test as succeeded, even if the completionHandler block is called later and trigger a (late) assertion failure because data is nil, but it would be too late.

Objective-C ```objc - (void)testFoo { NSURLRequest* request = ... [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse* response, NSData* data, NSError* error) { NSAssertNotNil(data, @"Received data should not be nil"); } ]; // The rest of the code below will continue to execute without waiting for the request to have its response } ```

The solution: use XCTestExpectation

With Xcode 6, XCTest now has a new feature, called XCTestExpectation, just to do that and to handle testing in asynchronous code. Mattt even talks about it here in one of its great NSHipster's articles.

The idea behind this is to:

  • Create an XCTestExpectation using the expectationWithDescription: method, giving it a nice custom description
  • Calling the waitForExpectationsWithTimeout:handler: method when you want to wait for your asynchronous calls to end
  • In your completionHandler or whatever method your asynchronous operation calls when it finishes, call fulfill on the previously created XCTestExpectation object to mark it as… fulfilled.

Then, as its name states, waitForExpectationsWithTimeout:handler: will wait for all previously-declared expectations to be fulfilled, or for the timeout to be reached, and call the block passed to its handler: parameter (if one has been provided). It will also automatically fail if there was some expectations left unfulfilled when the timeout was reached.

Objective-C ```objc - (void)testFoo { NSURLRequest* request = ... XCTestExpectation* responseArrived = [self expectationWithDescription:@"response of async request has arrived"]; __block NSData* receivedData = nil; [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse* response, NSData* data, NSError* error) { receivedData = data; [responseArrived fulfill]; } ];

[self waitForExpectationsWithTimeout:timeout handler:^{ // By the time we reach this code, the while loop has exited // so the response has arrived or the test has timed out XCTAssertNotNil(receivedData, @"Received data should not be nil"); }]; }

</details>

<details open>
<summary>Swift</summary>
```swift
func testFoo() {
  let request = NSURLRequest(...)
  let responseArrived = self.expectationWithDescription("response of async request has arrived")
  var receivedData: NSData?
  NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {
    (response: NSURLResponse!, data: NSData!, error: NSError!) in
    receivedData = data
    responseArrived.fulfill()
  }
    
  self.waitForExpectationsWithTimeout(timeout) { err in
    // By the time we reach this code, the while loop has exited
    // so the response has arrived or the test has timed out
    XCTAssertNotNil(receivedData, "Received data should not be nil")
  }
}

Common errors when not handling async tests properly

To end this article, one common error you may see if you [OHHTTPStubs removeAllStubs] at the end of your Unit Tests (for example in the tearDown method), but failed to wait for your asynchronous test to have a response before letting the test case end. In that case, you may encounter the issue described here. Be aware that when having this error, it probably means that you forgot to wait for your asynchronous operations to finish!