Testing Reactive Microservice in Spring Boot — Integration & Component Testing
By: Abhi Nandan
Tags: Automation Testing, Microservices, Continuous Delivery, Spring, Reactive Programming

Photo by Louis Reed on Unsplash

In the previous blog, we discussed some of the Unit Testing approaches Spring Boot Reactive stack using JUnit. Till this point, we have achieved the first phase of our Continuous Testing journey. In this part, we are going to discuss the next two phases -

Integration Testing, and

Component Testing

So, let’s get started!

Why do you need Integration & Component Testing?

Unit testing takes care of validating the implementation of the functional blocks (unit), however, it does not provide guarantee about the behavior of the system. To verify that each module correctly collaborates with its interacting modules more coarse-grained testing is required.

Defining Integration Tests

As Martin Fowler defines, “An integration test verifies the communication paths and interactions between components to detect interface defects”. In microservice architectures, integration tests are typically used to verify interactions between layers of integration code and the external components to which they are integrating.

An example of an integration test in our Reactive Microservice context could be calling the Application Router function and validate the integration between the different blocks (handler, processor etc.), and the external data source. Integration with data stores and external components provides early feedback on the system behavior.

Defining Component Tests

While the integration tests validate the interactions of the external modules & persistence layer (data-store), however, these tests do not take care of the business logic validations. The Component Tests are responsible to validate the business logic in the microservices. Although this can be achieved by the end-to-end system integration tests, SIT tests are slow and highly dependent on the external systems. The primary objective of the Component Tests is examining the system behavior in isolation. Separating the components from each other provides the advantage of thoroughly acceptance test the behavior of the component.

The thin line between Integration & Component Testing

While we will be discussing more around the implementation of the two testing phases, you’ll find that the implementation principles are very similar, however, it needs careful test designs to avoid duplication. While the integration tests focus more on validation of the integration interface code with the persistent layer and external systems, the component tests are focused towards the business functionality validations and its different variations.

Let us illustrate this by the following example.

A payment microservice interacts with a third-party payment gateway. The third-party service contract provides two different types of response structures (success and error response schema). In this case, the integration test should be validating the behavior of the system with one success and one error response, but when it comes to the component tests, the number of test cases will vary depending on the business logic that applies to different types of successful or failure responses.

Tools

In this series, we are discussing the Spring-boot 5 reactor tech stack, that supports reactive programming. The integration & component test phases require the following features from the test automation framework perspective -

Test Framework — JUnit 5

Reactive Rest Client — WebTestClient

Mocking framework — Mockito

Stubbing framework — WireMock

Designing Integration Tests

Let’s consider that there’s a DatabaseService class that implements the persistence layer. It also exposes a method called findById(id), that returns the document from the datastore. The following is an example of a MockBean injection to mimic the persistence layer in your test.

public class IntegrationTests { @MockBean

private DatabaseService databaseService; @BeforeEach

void mockCouch() {

when(databaseService.findById(any()))

.thenReturn(Mono.just(JsonDocument.create("Document_key",

JsonObject.fromJson("{your_json_response}"))));

}

}

Time to write the Integration Test!

To write an integration test, we will need a WebTestClient, to call the microservice. In this example test, we’ll call the microservice Router Function and validate that the response is received as OK. Here is how we can write the test -

@Test

@DisplayName("Should Get Success on valid request")

public void validRequestIntegrationTest() {



WebTestClient

.bindToRouterFunction(

applicationRouter.mainRouterFunction(applicationHandler, errorHandler, serviceLogger))

.build()

.post()

.uri("/base-path")

.accept(MediaType.APPLICATION_JSON)

.contentType(MediaType.APPLICATION_JSON)

.exchange()

.expectStatus()

.isOk()

.expectBody(ApplicationResponseDTO.class)

.consumeWith(serverResponse ->

assertNotNull(serverResponse.getResponseBody()));

}

Let’s go through this in details. This integration test calls the application router function of the microservice under test and validates the response received. For calling the service, a WebTestClient is instantiated with bindToRouterFunction method. This will create the POST request to the service and get the response back from the application. The response received is parsed to get the response body and validate further. This test achieves the validation of the integration interfaces with the datastore.

Last bit of the Integration Test!

To instantiate the ApplicationRouter class, the microservice application needs to be initialized. @SpringBootTest annotation can do the magic here!. So finally, the test looks like -

@SpringBootTest(classes = Application.class)

@ExtendWith(SpringExtension.class)

@ActiveProfiles("integration")

public class IntegrationTests { @Autowired private ApplicationRouter applicationRouter; @Autowired private ApplicationHandler applicationHandler; @Autowired private ErrorHandler errorHandler; @Autowired private ServiceLogger serviceLogger; @MockBean private DbService dbService; @BeforeEach

void mockCouch() {

when(dbService.findById(any()))

.thenReturn(Mono.just(JsonDocument.create("Document_key",

JsonObject.fromJson("{your_json_response}"))));

} @Test

@DisplayName("Should Get Success on valid request")

public void validRequestIntegrationTest() {



WebTestClient

.bindToRouterFunction(

applicationRouter.mainRouterFunction(applicationHandler, errorHandler, serviceLogger))

.build()

.post()

.uri("/base-path")

.accept(MediaType.APPLICATION_JSON)

.contentType(MediaType.APPLICATION_JSON)

.exchange()

.expectStatus()

.isOk()

.expectBody(ApplicationResponseDTO.class)

.consumeWith(serverResponse ->

assertNotNull(serverResponse.getResponseBody()));

}

}

Designing Component Tests

The test design principles of the Component Test is already discussed, and in this phase of testing the primary objective is validating the business logic of the microservice in isolation. The toolset for Component Test remains more or less the same. In this section, we will also touch upon the concepts of WireMock for stubbing.

The approach of writing the tests and design principles are highly subjective and depends largely on the context. The Component Tests can be implemented in the same way as the Integration Tests by design principle. We can also configure WebTestClient automatically to bind the application context and run the tests. This would provide more of a black-box approach to the testing, where the component under test is fed with the input data and response is functionally validated. The component testing phase is business-critical, as the objective is more functional.

Associating Profiles with the testing phase

Profile is one of the key features of the Spring framework, and it allows us to map beans to the different profiles (in loose terms can be called as environments as well). This feature enables us to create a component test profile that can have the configuration to stub all the external systems. This will provide us with the advantage of isolating our system under test from any external dependency.

Time to write Component Tests

A component test can have a lot of prerequisites. This can include

Starting Spring Application Registering Junit 5 extensions Configuring WebTestClient Configuring WireMock

We can create an annotation to complete all the above mentioned tasks before the beginning of the test. This can look like -

@Target({ ElementType.TYPE, ElementType.METHOD })

@Retention(RetentionPolicy.RUNTIME)

@Tag("ComponentTest")

@ExtendWith(SpringExtension.class)

@SpringBootTest(classes = Application.class)

@ActiveProfiles("component")

@AutoConfigureWebTestClient(timeout = "30000")

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

@AutoConfigureWireMock(port = 9900)

public @interface ComponentTest {

}

So let’s write a very simple component test using the @ComponentTest annotation -

@ComponentTest

public class ComponentTests {

@Autowired WebTestClient webTestClient;



@BeforeAll

void initStubs() {

WireMock.stubFor(post("/downstream/system")

.willReturn(aResponse()

.withStatus(200)

.withHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)

.withBody("{\"status\": \"success\"}")

.withBodyFile("ping-response.json")

));

}



@Test

void shouldGetSuccessOnValidCalls() {

MultiValueMap<String, String> headers = new HttpHeaders();

headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);



WebTestClient.ResponseSpec response = webTestClient

.post()

.headers(httpHeaders -> httpHeaders.addAll(headers))

.body(BodyInserters.fromValue("{\"key\":\"value\"}"))

.exchange();

response

.expectStatus()

.isEqualTo(HttpStatus.OK)

.expectBody(ApplicationResponseDTO.class)

.consumeWith(serverResponse -> {

ApplicationResponseDTO responseBody = serverResponse.getResponseBody();

//all assertions go here



});



}

}

So what’s happening in this test? This test first creates a stub for the external system in the @BeforeAll method, by running the same on port 9900 (as defined in the annotation). The ComponentTests class is a Junit class as our custom annotation @ComponentTest takes care of all the prerequisites. The test takes a simple payload as input and sends a request with all required headers and validates the response on execution. This test treats our component as black-box and validates the business requirements.

In this blog, we talked about integration and component test design principles. One of the key qualities behind the success of Continuous Testing is well-designed tests. Integration & Component tests allow you to identify the defects in the early part of the development cycle. These two testing phases also play an important role in reducing test execution time. In the next article, we will discuss some of the sample design patterns for maintaining large test suites. Stay tuned & Happy testing!