← back to blog

A Closure Encounter

2024-12-23

I recently got this in a conversation I had with chatGPT. I hazard to guess who it should really be attributed to. If you wrote this originally (or summink v similar) please reach out.

import XCTest

class APITests: XCTestCase {

    func testFetchData() {
        // Create the expectation
        let expectation = self.expectation(description: "Fetching data from API")

        // Simulate an asynchronous API call
        fetchDataFromAPI { success in
            XCTAssertTrue(success, "API call should succeed")
            expectation.fulfill() // Fulfill the expectation when done
        }

        // Wait for the expectation with a timeout
        wait(for: [expectation], timeout: 5.0)
    }

    // Mock API function
    private func fetchDataFromAPI(completion: @escaping (Bool) -> Void) {
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
            completion(true) // Simulating a successful API call
        }
    }
}

The original prompt was searching for a bit of clarity on how to implement expectations in testing Swift code. What it gave me was a bit of clarity on a few topics that had given me head-scratching moments in the past, namely: expectations, mocks, and escaping closures...

Here's a rundown...

let expectation = self.expectation(description: "Fetching data from API")

Here, we're creating an expectation that will keep our test waiting until something specific happens (or until it times out). It's like telling XCTest, "I've got an async thing to do so don't call it a failure just because it doesn't finish instantly." The description is just for logging purposes, so make it meaningful for debugging.

fetchDataFromAPI { success in
    XCTAssertTrue(success, "API call should succeed")
    expectation.fulfill()
}

This is where we simulate our API call. The fetchDataFromAPI function is asynchronous, so we use a closure (in this case, a callback function) to handle the result.

wait(for: [expectation], timeout: 5.0)

This line is like putting the test on pause, giving the async code some time to do its thing. The timeout is how long we're willing to wait before we give up and call it quits. If the expectation isn't fulfilled in time, the test fails. Five seconds is a reasonable limit for most API tests.

private func fetchDataFromAPI(completion: @escaping (Bool) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
        completion(true) // Simulating a successful API call
    }
}

This little helper function is our stand-in for a real API call. It's asynchronous because that's how network requests work in the wild. The @escaping keyword tells Swift, "This closure is going to hang around after the function ends, so don't clean it up just yet."

Inside, we're using DispatchQueue.global().asyncAfter to mimic the delay you'd get from an actual network request. After 2 seconds, we call completion(true) to simulate a successful response. If you wanted to test failures, you could pass false or even throw in some randomization.