Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

iOS Unit Testing: Wait for Time Interval with Expectations

I'm trying to update my asynchronous unit tests to use the new XCTestExpectation interface instead of manually spinning the run loop.

My unit tests previously utilized the functions waitForBlock, finishBlock, and waitForTimeInterval: which is simply a convenience method that called finishBlock after the specified time. I'm trying to update this setup to use expectations.

The tests that were utilizing waitForBlock + finishBlock semantics are all working just as expected after being replaced with waitForExpectationsWithTime:handler: and fulfill, but my solution to replace waitForTimeInterval: doesn't seem to be working.

- (void)waitForTimeInterval:(NSTimeInterval)delay
{
    XCTestExpectation *expectation = [self expectationWithDescription:@"wait"];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [expectation fulfill];
    });

    [self waitForExpectationsWithTimeout:delay + 1 handler:nil];
}

Edit:

Seems like that code actually does work... so this was probably just Xcode 6 screwing with me this afternoon.


I feel like it should be fairly straight-forward: Create an expectation, set up an asynchronous block that fulfills is, and wait. However, the dispatch_after block is never invoked.

My hunch is that waitForExpectationsWithTimeout:handler: blocks its current thread, which is the main queue, so the run loop never gets around to its asynchronous blocks. That seems reasonable, but I'm having trouble coming up with a different way to implement this functionality.

I'm looking for either 1) additional information about XCTestExpectation that might reveal a workaround, or 2) a different idea for implementing this functionality.

like image 928
LeffelMania Avatar asked Sep 25 '14 20:09

LeffelMania


People also ask

What does the timely rule of unit testing mean?

Timely: Unit tests should be written just before the production code that makes the test pass. This is something that you would follow if you were doing TDD (Test Driven Development), but otherwise it might not apply.

How long should unit tests run?

Still, it seems as though a 10 second short-term attention span is more or less hard-wired into the human brain. Thus, a unit test suite used for TDD should run in less than 10 seconds. If it's slower, you'll be less productive because you'll constantly lose focus.

What is Xctestcase?

The primary class for defining test cases, test methods, and performance tests. Xcode 7.2+

How often should unit tests be executed?

Run all your unit tests as often as possible, ideally every time the code is changed. Make sure all your unit tests always run at 100%. Frequent testing gives you confidence that your changes didn't break anything and generally lowers the stress of programming in the dark.


2 Answers

In one of my Unit test cases I needed to test the running of a method in my main app code, which should trigger a timer in about 1 second to call another method in the app. I used XCTestExpectation wait and DispatchQueue.asyncAfter as a mechanism to stop and wait before I check the result. The following code is a snippet in Swift 3 / 4:

    <call the main app method which will trigger a timer event>

    // wait
    let expectation = XCTestExpectation(description: "test")
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
        expectation.fulfill()
    }
    wait(for: [expectation], timeout: 2.5)

    <check the result of the timer event>
like image 66
CodeBrew Avatar answered Sep 19 '22 07:09

CodeBrew


dispatch_async(dispatch_get_main_queue(), ...) doesn't seem to work in Xcode 7 UI Tests, the completion handler is never called. performSelector is no longer available in Swift, but there are two other workarounds:

  1. Using a timer

    var waitExpectation: XCTestExpectation?
    
    func wait(duration: NSTimeInterval) {
        waitExpectation = expectationWithDescription("wait")
        NSTimer.scheduledTimerWithTimeInterval(duration, target: self,
            selector: Selector("onTimer"), userInfo: nil, repeats: false)
        waitForExpectationsWithTimeout(duration + 3, handler: nil)
    }
    
    func onTimer() {
        waitExpectation?.fulfill()
    }
    
  2. Run the block on global queue (it works, but probably unsafe as it's not documented anywhere that XCTestExpectation is thread-safe).

    func wait(duration: NSTimeInterval) {
        let expectation = expectationWithDescription("wait")
        let dispatchTime = dispatch_time(DISPATCH_TIME_NOW,
            Int64(duration * Double(NSEC_PER_SEC)))
        dispatch_after(dispatchTime,
            dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
            expectation.fulfill()
        }
        waitForExpectationsWithTimeout(duration + 3, handler: nil)
    }
    
like image 22
Zmey Avatar answered Sep 22 '22 07:09

Zmey