Search code examples
swiftgrand-central-dispatch

Using Dispatch.main in code called from XCTestCase does not work


I have a function that is a async wrapper around a synchronous function.

The synchronous function is like:

class Foo {
    class func bar() -> [Int] {
        return [1,2,3]
    }

    class func asyncBar(completion: @escaping ([Int]) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            let intArray = bar()
            DispatchQueue.main.async {
                completion(intArray)
            }
        }
    }
}

When I call it from a XCTestCase completion does not run.

Is there some sort of perverse interaction between the way unit tests are done in XCode and the main thread?

I can find no documentation on the Web about this.

I have to use the main thread for the callback as it interacts with the Gui.

My test case looks something like:

func testAsyncBar() throws {
    var run = true
    func stopThisThing(ints: [Int]) {
        run = false
    }
    Foo.asyncBar(completion: stopThisThing)
    while run {
        print ("Running...")
        usleep(100000)
    }
}

The busy loop at the end never stops.


Solution

  • Your test’s while loop will block the main thread (if the test runs on the main thread). As such, the closure dispatched via DispatchQueue.main.async can never run, and your run Boolean will never get reset. This results in a deadlock. You can confirm this by printing Thread.isMainThread or adding a test like dispatchPrecondition(condition: .onQueue(.main)).

    Fortunately, unit tests have a simple mechanism that avoids this deadlock. If you want unit test to wait for some asynchronous process, use an XCTestExpectation:

    func testAsyncBar() throws {
        let e = expectation(description: "asyncBar")
    
        func stopThisThing(ints: [Int]) {
            e.fulfill()
        }
    
        Foo.asyncBar(completion: stopThisThing)
    
        waitForExpectations(timeout: 5)
    }
    

    This avoids problems introduced by the while loop that otherwise blocked the thread.

    See Testing Asynchronous Operations with Expectations.