Component tests check full use cases from start to finish. They’re essential for verifying and documenting how an application or service behaves overall. But they can be expensive, especially in terms of setup and execution time — which is why it’s important to define their scope carefully.
That said, I’ve often found them cost-effective in distributed architectures. They’re usually simple to set up since you can reuse the service’s external API without needing extras like fake servers. And since each microservice tends to have a narrow focus, you can test behavior thoroughly in isolation.
This article explores how to make component tests more robust — with one key idea: keep them independent of implementation details.
The following example shows a Gherkin specification for a booking HTTP API that is very coupled to the technical implementation:
Scenario: Get an error when trying to book a hotel with no vacancy
Given Get to hotel service "/api/hotel/1234" returns a response with the status code 200 and the body:
"""
{
"id": "1234",
"name": "Ritz",
"availableRooms": 0
}
"""
Given Get to user service "/api/user/456" returns a response with the status code 200 and the body:
"""
{
"id": "456",
"firstName": "John",
"lastName": "Doe"
}
"""
When I post "http://my.app.fr:8080/booking/api/" on the "booking" application with the following body:
"""
{
"userId": "456",
"hotelId": "1234",
"from": "2017-09-16",
"to": "2017-09-24"
}
"""
Then I get a response with the status code 200
And I get a JSON response with the body:
"""
{
"errors" : [ {
"code" : 12,
"message" : "There is no room available for this booking request"
} ]
}
"""
This is the associated Java code to the first step (the framework used is Cucumber):
@Given("^Get to hotel service \"([^\"]*)\" returns a response with the status code (\\d+) and the body:$$")
public void getToHotelServiceReturnsAResponseWithTheStatusCodeAndTheBody(String uri, int statusCode, String body) {
hotelWireMockServer.stubFor(get(urlEqualTo(uri))
.willReturn(aResponse()
.withStatus(statusCode)
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(body)
)
);
}
It is possible to make the test more explicit and functional. For instance, instead of describing HTTP calls and responses in the steps, it is possible to write them in plain English. Thus, the first steps in the Gherkin file can be replaced by:
Given There is no vacancy for the hotel "Ritz" of id 1234
Given The following users exist:
| id | firstName | lastName |
| 456 | John | Doe |
The associated Java code is now the following:
@Given("^There is no vacancy for the hotel \"([^\"]*)\" of id (\\d+)$")
public void thereIsNoVacancyForTheHotelOfId(String name, Long id) {
hotelWireMockServer.stubFor(get(urlEqualTo("/api/hotel/" + id))
.willReturn(aResponse()
.withStatus(statusCode)
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody("{ \"id\": \"" + id + "\", \"name\": \"" + name +"\", \"availableRooms\": 0 }")
)
);
}
@Given("^The following users exist:$")
public void theFollowingUsersExist(List<UserDTO> users) {
users.forEach(
user -> userWireMockServer.stubFor(get(urlEqualTo("/api/user/" + user.id))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.withBody(user.asJson())
)
)
);
}
We notice that the purely technical details like the URL, the JSON response and the HTTP status are now specified in the Java code. This allows making the Gherkin specification more focused on the behaviour, clearer and more concise. Hence, this test is now more maintainable and robust.
The initial test is now the following:
Scenario: Get an error when trying to book a hotel with no vacancy
Given There is no vacancy for the hotel "Ritz" of id 1234
Given The following users exist:
| id | firstName | lastName |
| 456 | John | Doe |
When I post "http://my.app.fr:8080/booking/api/" on the "booking" application with the following body:
"""
{
"userId": "456",
"hotelId": "1234",
"from": "2017-09-16",
"to": "2017-09-24"
}
"""
Then I get a response with the status code 200
And I get a JSON response with the body:
"""
{
"errors" : [ {
"code" : 12,
"message" : "There is no room available for this booking request"
} ]
}
"""
Since this microservice is an HTTP API, keeping the When
and Then
in a technical form can be relevant. Indeed, one can argue that the HTTP status and the format of the exchanged messages are part of its behaviour.
Conclusion
A component test must explicitly describe a real use case. To do that, it is important to make it as independent as possible of the implementation. This article shows a way to go from a test highly coupled to the implementation to a more functional and concise one.