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.
- The XCTAssertTrue(success, "API call should succeed") line is your standard test check. It's making sure the result is what we expect; true, in this case.
- Then we call expectation.fulfill(), which signals that the async task has done its thing. Without this, the test will just hang around and eventually fail when the timeout hits.
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.