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 its 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 tightly 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 for 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, you can 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 technical details, such as the URL, the JSON response, and the HTTP status, are now specified in the Java code. This makes the Gherkin specification more focused on the behaviour, clearer, and more concise. As a result, this test is now more maintainable and robust.

The revised 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 crucial to make it as independent as possible from the implementation. This article shows a way to go from a test that is highly coupled to the implementation to a more functional and concise one.