Search code examples
swifttestingxctestswift-concurrency

XCTest async code doesn't get executed in 'tearDown() async throws' method


From Apple's XCTest doc regarding setUp and tearDown it said to perform async code in tearDown() async throws method.

And in my case, I create some test data in server with API call, and I'd like to clean it after the test case execution is completed. The ideal place is tearDown from my understanding.

override func tearDown() async throws {
    // This is the tearDown() async instance method.
    // XCTest calls it after each test method.
    // Perform any asynchronous per-test cleanup here.
}

Then I have a small test, the result is this tearDown() async throws method is called after test case execution, but print code in DispatchQueue print("~ make an API call") is never executed, and the test is completed soon. Although I think this method is designed to handle async scenario from what Apple doc said, for instance cleaning server data with API call.

    override func tearDown() async throws {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            print("~ make an API call")
        }
    }
    
    // tearDown test method
    func testDemo() {
        print("~ Test started")
    }

Thus, my question is what is the property way to clean test data asynchronously in tearDown method?


Solution

  • The problem is that you are mixing an old-style DispatchQueue-based async method with the new, language feature, async method.

    From the type systems perspective, DispatchQueue.main.asyncAfter is a sync method, since it isn't marked as async, hence when you call it from an async method, the async method will treat is as a synchronous method and continue execution right after that method returns, rather than waiting for its completion to be executed asynchronously.

    You can resolve this issue several ways. You can either wrap the completion-based DispatchQueue.asyncAfter method in a Continuation and hence turn it into an actual async method or you can simply use Task.sleep.

    override func tearDown() async throws {
      try await Task.sleep(for: .seconds(2))
      // make the API call here
    }