Adding automated microservice tests in isolation
Before we wrap up the implementation, we also need to write some automated tests.
We don’t have much business logic to test at this time, so we don’t need to write any unit tests. Instead, we will focus on testing the APIs that our microservices expose; that is, we will start them up in integration tests with their embedded web server and then use a test client to perform HTTP requests and validate the responses. With Spring WebFlux comes a test client, WebTestClient
, that provides a fluent API for making a request and then applying assertions on its result.
The following is an example where we test the composite product API by doing the following tests:
- Sending in
productId
for an existing product and asserting that we get back 200 as an HTTP response code and a JSON response that contains the requestedproductId
along with one recommendation and one review - Sending in a missing
productId
and asserting that we get back 404 as an HTTP response code and a JSON response that contains relevant error information
The implementation for these two tests is shown in the following code. The first test looks like this:
@Autowired
private WebTestClient client;
@Test
void getProductById() {
client.get()
.uri("/product-composite/" + PRODUCT_ID_OK)
.accept(APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(APPLICATION_JSON_UTF8)
.expectBody()
.jsonPath("$.productId").isEqualTo(PRODUCT_ID_OK)
.jsonPath("$.recommendations.length()").isEqualTo(1)
.jsonPath("$.reviews.length()").isEqualTo(1);
}
The test code works like this:
- The test uses the fluent
WebTestClient
API to set up the URL to call “/product-composite/
"+ PRODUCT_ID_OK
and specify the accepted response format, JSON. - After executing the request using the
exchange()
method, the test verifies that the response status isOK
(200
) and that the response format actually is JSON (as requested). - Finally, the test inspects the response body and verifies that it contains the expected information in terms of
productId
and the number of recommendations and reviews.
The second test looks as follows:
@Test
public void getProductNotFound() {
client.get()
.uri("/product-composite/" + PRODUCT_ID_NOT_FOUND)
.accept(APPLICATION_JSON_UTF8)
.exchange()
.expectStatus().isNotFound()
.expectHeader().contentType(APPLICATION_JSON_UTF8)
.expectBody()
.jsonPath("$.path").isEqualTo("/product-composite/" +
PRODUCT_ID_NOT_FOUND)
.jsonPath("$.message").isEqualTo("NOT FOUND: " +
PRODUCT_ID_NOT_FOUND);
}
One important note regarding this test code is that this negative test is very similar to the preceding test in terms of its structure; the main difference is that it verifies that it got an error status code back, Not Found
(404
), and that the response body contains the expected error message.
To test the composite product API in isolation, we need to mock its dependencies, that is, the requests to the other three microservices that were performed by the integration component, ProductCompositeIntegration
. We use Mockito to do this, as follows:
private static final int PRODUCT_ID_OK = 1;
private static final int PRODUCT_ID_NOT_FOUND = 2;
private static final int PRODUCT_ID_INVALID = 3;
@MockitoBean
private ProductCompositeIntegration compositeIntegration;
@BeforeEach
void setUp() {
when(compositeIntegration.getProduct(PRODUCT_ID_OK)).
thenReturn(new Product(PRODUCT_ID_OK, "name", 1, "mock-address"));
when(compositeIntegration.getRecommendations(PRODUCT_ID_OK)).
thenReturn(singletonList(new Recommendation(PRODUCT_ID_OK, 1,
"author", 1, "content", "mock address")));
when(compositeIntegration.getReviews(PRODUCT_ID_OK)).
thenReturn(singletonList(new Review(PRODUCT_ID_OK, 1, "author",
"subject", "content", "mock address")));
when(compositeIntegration.getProduct(PRODUCT_ID_NOT_FOUND)).
thenThrow(new NotFoundException("NOT FOUND: " +
PRODUCT_ID_NOT_FOUND));
when(compositeIntegration.getProduct(PRODUCT_ID_INVALID)).
thenThrow(new InvalidInputException("INVALID: " +
PRODUCT_ID_INVALID));
}
The mock implementation works as follows:
- First, we declare three constants that are used in the test class:
PRODUCT_ID_OK
,PRODUCT_ID_NOT_FOUND
, andPRODUCT_ID_INVALID
. - Next, the
@MockitoBean
annotation is used to configure Mockito to set up a mock for theProductCompositeIntegration
interface. - If the
getProduct()
,getRecommendations()
, andgetReviews()
methods are called on the integration component, andproductId
is set toPRODUCT_ID_OK
, the mock will return a normal response. - If the
getProduct()
method is called withproductId
set toPRODUCT_ID_NOT_FOUND
, the mock will throwNotFoundException
. - If the
getProduct()
method is called with productId set toPRODUCT_ID_INVALID
, the mock will throwInvalidInputException
.
The full source code for the automated integration tests on the composite product API can be found in the ProductCompositeServiceApplicationTests.java
test class.
The automated integration tests on the API exposed by the three core microservices are similar, but simpler since they don’t need to mock anything! The source code for the tests can be found in each microservice’s test folder.
The tests are run automatically by Gradle when performing a build:
./gradlew build
You can, however, specify that you only want to run the tests (and not the rest of the build):
./gradlew test
This was an introduction to how to write automated tests for microservices in isolation. In the next section, we will learn how to write tests that automatically test a microservice landscape. In this chapter, these tests will only be semi-automated. In upcoming chapters, the tests will be fully automated, which is a significant improvement.