Test Stub
25 Dec 2021You can check out the PlayGround used in the explanation.
1. What is Stub?
In this article, we will discuss Stub
, one of the representative Test Doubles.
A Stub
is an object used to provide ‘indirect input’ to the subject of testing (SUT
, System Under Test). In most cases, objects or functions (Units) are designed to return output based on the input they receive. When we write tests, we naturally verify whether the output matches the input we provide. In this context, a tester constructs various inputs to check if the test appropriately validates the Unit. These provided inputs are referred to as ‘indirect input.’ Furthermore, providing ‘indirect input’ to the SUT
can be structured in two ways: directly injecting it into the Unit or providing it from another object. This other object, referred to as a ‘DOC’ (Depended on Component) because it is an object that the SUT
depends on, and a Stub
is an object that provides the input from this ‘DOC.’
- When the behavior of the
SUT
depends on input values, these inputs are called ‘indirect input.’ - A
Stub
is an object used for providing ‘indirect input’ to theSUT
.
2. Using Stubs for Unit Testing
Typical testing is divided into four phases: (setup
, exercise
, verify
, teardown
).
- During the Setup phase, the test author decides whether to use
Stub
s when providing input to theSUT
. In other words, they implement the interfaces of the DOCs on which theSUT
depends. - During the Exercise phase, the Stub provides the appropriate indirect input to the
SUT
when theSUT
requests it. - During the Verify phase, the test author checks whether the state of the
SUT
is appropriate.
To illustrate this in more detail, let’s consider an example. Let’s say we have a SubscriptionWorker
that handles an HTTP API for toggling a user’s program subscription on/off. This API provides the following JSON response:
{
"subscribed": true,
"subscriptionCount": 27038
}
To process this JSON, we can structure the SubscriptionWorker
as follows:
View Detailed Code
We make calls to the Subscription.Worker.update(request:completion:)
function as follows:
let urlRequest = URLRequest(url: URL(string: "https://hcn1519.github.io")!)
Subscription.Worker.update(request: .init(urlRequest: urlRequest), completion: { result in
// do something
print(result)
})
In this scenario, we want to test the Subscription.Worker.update(request:completion:)
function. More specifically, we want to test whether it correctly converts the subscription information obtained through an HTTP request into a usable model in the app. However, there’s an issue: the update()
function makes a call to dataTask(with:completionHandler)
, making it dependent on an external server. This means that to create various responses, you would need the server to provide them.
In such cases, using a Stub
allows you to perform the desired tests without relying on the server.
View Detailed Code
The main difference between the previous code and the modified code is the ability to inject a Stub
. If you use Stub injection, the update()
function will return a StubResponse
that you directly provide, without making actual requests to the URL. In other words, you can provide the response you want directly through the Stub
, even if the server does not provide the desired response.
import UIKit
import XCTest
func testSuccess() {
let urlRequest = URLRequest(url: URL(string: "https://hcn1519.github.io")!)
let successData = """
{
"subscribed": true,
"subscriptionCount": 27038
}
""".data(using: .utf8)!
let successURLResponse = HTTPURLResponse(url: urlRequest.url!,
statusCode: 200,
httpVersion: nil,
headerFields: [:])!
let successResponse = Stub.Response(response: successURLResponse,
result: .success(successData))
let successStub = Stub.response(successResponse)
let successRequest = Subscription.Request(urlRequest: urlRequest,
stub: successStub)
Subscription.Worker.update(request: successRequest, completion: { result in
switch result {
case .success(let response):
XCTAssert(response.subscribed == true)
XCTAssert(response.subscriptionCount == 27038)
print("\(#function) success")
case .failure(let error):
XCTAssert(false, "Result should succeed \(error.localizedDescription)")
}
})
}
Note: Here, we have written the code directly to demonstrate how Stubs work. In actual code development, you can make use of libraries like Moya, which incorporate this functionality quite effectively.
3. When to Use Stubs
Stub
can be used when it’s difficult to control the injection of ‘indirect input’ into the SUT
. For example, you can use Stub
to create various responses, such as successful and failed cases, to control the behavior of the SUT
. Additionally, in test environments where accessing certain modules (e.g., a payment module) is challenging, you can configure those modules to provide values through Stub
for testing purposes.