Using Stubs in Testing with Async/Await
If you haven’t already, feel free to understand Stubs in detail in my article on stubs.
In this article, we’ll go through an example whereby we explore how to use stubs with async/await
in Swift. I use the same example that i used in the article i shared above however, that was with completion closures
and this is with async/await
Example: HTTP Client Stub with Async/Await
The Scenario
Imagine you have an HTTPClient
protocol for performing network requests asynchronously. To test a service dependent on this client, we’ll use a stub to control the response.
Step 1: Define the HTTPClient Protocol
protocol HTTPClient {
func get(from url: URL) async throws -> Data
}
This protocol defines a single asynchronous method, get(from:)
, which performs a GET request and either returns Data
or throws an error.
Step 2: Create the HTTPClient Stub
The HTTPClientStub
conforms to the HTTPClient
protocol and allows you to define a controlled response using async/await
.
class HTTPClientStub: HTTPClient {
var stubbedResponse: Result<Data, Error>
func get(from url: URL) async throws -> Data {
switch stubbedResponse {
case .success(let data):
return data
case .failure(let error):
throw error
}
}
}
Here, the stubbedResponse
property holds the predefined result (either success or failure) returned when the get(from:)
method is called.
Step 3: Configure the Stubbed Response
You can configure the stub with specific responses for your tests:
- Simulating a Successful Response
let httpClientStub = HTTPClientStub(stubbedResponse: .success(Data("Expected data".utf8)))
- Simulating an Error Response
let httpClientStub = HTTPClientStub(stubbedResponse: .failure(NSError(domain: "Test", code: 1, userInfo: nil)))
Using the Stub in Tests
Let’s use the HTTPClientStub
in a test for a service that relies on asynchronous HTTP requests.
Example Test
Suppose you have a RemoteLoader
that fetches data using the HTTPClient
:
class RemoteLoader {
private let client: HTTPClient
init(client: HTTPClient) {
self.client = client
}
func load(from url: URL) async throws -> Data {
return try await client.get(from: url)
}
}
You can test the RemoteLoader
using the stub.
- Test for a Successful Response
func testLoad_SuccessfulResponse_ReturnsData() async throws {
// Arrange
let expectedData = Data("Expected data".utf8)
let stub = HTTPClientStub(stubbedResponse: .success(expectedData))
let loader = RemoteLoader(client: stub)
let url = URL(string: "https://example.com")!
// Act
let receivedData = try await loader.load(from: url)
// Assert
XCTAssertEqual(receivedData, expectedData, "The loader should return the stubbed data.")
}
- Test for an Error Response
func testLoad_FailureResponse_ThrowsError() async throws {
// Arrange
let expectedError = NSError(domain: "Test", code: 1, userInfo: nil)
let stub = HTTPClientStub(stubbedResponse: .failure(expectedError))
let loader = RemoteLoader(client: stub)
let url = URL(string: "https://example.com")!
// Act & Assert
do {
_ = try await loader.load(from: url)
XCTFail("Expected an error but got success")
} catch {
XCTAssertEqual(error as NSError, expectedError, "The loader should throw the stubbed error.")
}
}
Benefits of Using Stubs with Async/Await
- Controlled Behavior: Stubs allow you to simulate both success and failure scenarios for asynchronous dependencies.
- Faster Tests: Avoid slow or unpredictable real API calls by simulating responses locally.
- Focused Testing: Isolate the component being tested without interference from external systems.
- Ease of Use: With
async/await
, handling stubs in tests is simpler and more readable compared to callback-based approaches.
Conclusion
Stubs are an essential part of writing effective tests, especially in the context of asynchronous programming. By simulating predefined responses, they enable you to isolate and verify the behavior of your code under controlled conditions. In the example above, the HTTPClientStub
demonstrates how stubs can be used with async/await
to test components that depend on asynchronous operations. With proper use of stubs, you can write tests that are reliable, fast, and easy to understand.