XCTestExpectation via NSPredicate Is On a Slow Interval, Use This Instead

You and I are probably pretty familiar with the XCTestExpectations helper expectation(description:) that we can .fulfill() in an async callback, and then wait in the test case for that to happen.

When you have neither async callbacks or notifications handy from which you can create test expectations, then there’s a NSPredicate-based version, too. Like all NSPredicates, it works with key–value-coding, or with block-based checks. These run periodically to check if the predicate resolves to “true”.

Here’s the output of print(Date()) inside the predicate block during the wait for the expectation to resolve:

2021-01-29 11:04:57 +0000
2021-01-29 11:04:58 +0000
2021-01-29 11:04:59 +0000
2021-01-29 11:05:00 +0000
2021-01-29 11:05:01 +0000
2021-01-29 11:05:02 +0000
2021-01-29 11:05:03 +0000
2021-01-29 11:05:04 +0000
2021-01-29 11:05:05 +0000
2021-01-29 11:05:06 +0000
2021-01-29 11:05:07 +0000
2021-01-29 11:05:08 +0000

You see, there’s one log statement per second, no more, no less.

I found that when the timeout of XCTestCase.wait is 1 second, you get one call. For 1.2 seconds, still just one. As far as I could observe, the periodic checking happened about after a second. I admit I didn’t check every edge case with an atomic clock because this is instructive already.

See, I’d have expected something more elaborate, maybe like this: check the predicate immediately, and then on an interval, and then at the point of the timeout, just in case. I know that checking a predicate that takes forever to evaluate at the timeout will make tests run even longer and not be what the programmer is expecting. But anything more elaborate than “check every whole second, ignore fractions of a second” would’ve been nice.

But since you cannot control when these interval checks are performed, in one situation I was stuck with timeouts of roughly two seconds, even though I knew that something smaller would be enough. Anything below 1.5s failed almost always. Taking multiple seconds for a simple async state change assertion wasn’t acceptable, though.

So these predicate checks aren’t the best for this job.

Daniel Alm brought up the idea to check in a loop, instead, since the expectation is to finish the callbacks pretty quickly anyway if only the predicates were checked more frequently. How many iterations would be wasted there, checking the condition? A handful, a couple dozen? It’s not like we expected the process to take long, so it made sense to try that manual loop approach and see how much faster it’d go.

I almost went with that. Here’s a timer-based approach that appears to work fine:

func wait(for block: @escaping () -> Bool, timeout: TimeInterval = 0.1) {
    let blockExpectation = expectation(description: "Block-based expectation")
    let timer = Timer.scheduledTimer(withTimeInterval: 0.02, repeats: true) { _ in
        if block() {
            blockExpectation.fulfill()
        }
    }
    wait(for: [blockExpectation], timeout: timeout)
    timer.invalidate()
}

You can increase the timer’s interval if you know the process will take longer to take some load of the run loop; but for the simple checks I had to make, I was able to reduce the total test duration (without the overhead of getting to execute the test case) from ~5s to ~0.5s in the worst case, i.e. when the waiting time runs out.