Testing a Spring Boot Microservices: Tools and Techniques

Nagasudeep V
KTH Distributed Systems
19 min readApr 25, 2020

By Gibson Chikafa, Muhammad Jahangir Zafar, Nagasudeep Vemula.

Fig 1:The Test Pyramid. We will be covering the Unit,Component and Integration Test layers in our Tutorial

In this tutorial we will cover how to write unit, integration and component tests for a Spring Boot Application. The code for the tutorial can be downloaded from the following github link: https://github.com/gibchikafa/springbootmicroservices

Expected Learning Outcomes

By the end of this tutorial, we would like the reader to get the following points as takeaways

  1. Appreciate the importance of testing and Test Driven Development (TDD)
  2. Different levels of testing and why we need them.
  3. Greater understanding of Tools and Libraries to write and run tests.
  4. How to test different layers of the MVC stack.
  5. Annotations to use on test classes for different tests
  6. Mocking classes, how and why is it done.
  7. Create configurations or profiles for different types of tests.
  8. How to do better integration testing with WireMock and Testcontainers.

Background

We would like to give a brief background of Spring and tests abstractions before we begin. If you are already familiar with Spring and have enough background on different types of tests you can skip this section.

The Spring Framework

Spring is the most popular application development framework for enterprise Java. At its core, Spring framework is a dependancy injection container, with a couple of convenience layers such as database access, proxies, Aspect-Oriented Programming, Remote Procedural Call (RPC), and a web mvc framework added on top. It helps you build Java application faster and more conveniently.

What is Spring Boot

Spring Boot is an open source Java framework built on top of the existing Spring framework. It provides an easier and faster way to set up,configure and run both simple and web-based applications. Spring Boot upholds the importance of testing so much that a suite of test libraries (e.g JsonPath, JUnit5, AssertJ, Mockito etc) come pre-configured. Spring Boot We have utilized this framework in developing our application for this tutorial.

Test Driven Development - Why is it Important?

Test Driven Development(TDD) is a modern paradigm that ensures high reliability of software products. In this process of software development the requirements are reworked into specific test cases that in essence ensure full functionality. The code is thus improved to pass these tests and in the process the product delivered is of a much greater quality. In our tutorial we will be using TDD to test our application in a thorough manner. The tests we will be running on our application are namely- unit tests,component tests and integration tests and we will give a short background on the importance of each of these.

Unit Testing

Take a look at fig 1 at the start of this article, the test pyramid. Unit tests occur at the very base of our pyramid. They form the foundation of any testing paradigm. The pyramid representation also means as we write more tests in the lower layers and as we go up the number of tests we write decreases.

A unit test is used to verify the smallest piece of testable software in the application to determine whether it behaves as expected.

Fig 2: Symbolic Depictions of Unit Tests for a Module

Often, difficulty in writing a unit test can highlight when a module should be broken down into independent more coherent pieces and tested individually. Thus, alongside being a useful testing strategy, unit testing is also a powerful design tool, especially when combined with test driven development. It is important to constantly question the value a unit test provides versus the cost it has in maintenance or the amount it constrains your implementation. By doing this, it is possible to keep the test suite small, focused and high value.

For our application we will be carrying out unit tests on the service layer and on the rest controller.

Fun Fact: As a part of my earlier role as a junior software developer I once wrote 8000 lines of unit tests just for an application with 2000 lines of code just for the sake of attaining good code coverage which was the only requirement my superiors wished to see. Needless to say the product delivered was not properly tested and ended up being highly unreliable.

Component Testing

In a microservice architecture, the components are the services themselves. We would like to test that different parts of the service (controller, service layer, and repository) work together by isolating third-party code and services. Isolation of the service is achieved by replacing external services (e.g other microservices that a microservices communicate with) with mocks and in memory database for external datastore. Using an in memory database also ensures that we are not manipulating real system data.

Integration Testing

Integration tests are used for the verification of the different communication paths between the modules of an application. They are mainly used for catching interface defects.

For microservices architectures, integration tests are typically focused on verifying the interaction between subsystems in charge of communicating with external components such as data stores and/or other (micro)services.

There are 2 major types of integration tests and we shall be carrying both of them out in this tutorial.They are:

  1. Gateway integration tests
  2. Persistence integration tests

1)Gateway Integration Testing

In gateway integration testing we would like to verify if our interface to the external microservice(s) is fully functional. In this tutorial steps we will show how this can be done and what tools we are using for the same.

2)Persistence Integration Tests

These tests are primarily done to verify that our external datastore works. Tests are done to check if we can connect to it, save and retrieve data. In addition we would also like to check if the schema retrieved matches the one assumed in the code. We will therefore try to save data to the datastore, retrieve the data, and check if the schema returned is the same as to what we expect. We will be using MySQL so make sure you have MySQL installed.

Our Application

In this section, we aim to give small background on our demo application to provide context to the reader.

As students living in the corridor rooms setup, there is a common task that we all share which is the cleaning of the kitchen and this duty gets passed around weekly. We developed a simple application for managing this schedule.

The application is divided into two microservices: accommodation and cleaningschedule. The accommodation service is responsible for storing and managing the records of the students and the rooms they live in and the cleaningschedule microservice communicates with the accommodation service to get the residents of the rooms in the corridor.

Each of our microservices has the following layers:

  1. Domain: Consists of our domain objects. The objects are Room, Occupant, and Schedule.
  2. Repository: Also known as the Persistence layer, repository layer handles both how to read and write data(delete, update, save and other queries) from and to the storage. It is also responsible for mapping data from the storage format to business objects i.e serialization. Spring Framework comes with JpaRepository interface that implements most of the queries for our domain objects so we do need to implement our own custom queries.
  3. Service: Encapsulate business logic into a single place to promote code reuse and separations of concerns.
  4. Presentation: Intercepts incoming requests and converts the payload of the request to the internal structure of the data i.e converting the request to some Domain object. Passes the request to the Service layer for further processing. Gets processed data from the Service layer and return the response to client. Maybe, this description rings a bell of a Controller. Yes you are right, the Controller resides in the presentation layer.
  5. Network: Responsible for communicating with other microservices.

The Toolkit

In this section we will give a brief description on the libraries and frameworks used to write and execute the tests.

IDE (Integrated Development Environment)

We used Eclipse but really any IDE out there would work to complete this tutorial. However, we do recommend Eclipse or IntelliJ for the support they offer. Eclipse is one of the popular IDEs that is easy to use and has full support of Java programming language.

JUnit5

The one stop in testing frameworks when it comes to writing tests for Java language. JUnit allows as to do automated testing rather than writing output to the console and later trying to verify it manually. JUnit5 is the latest version of JUnit library. Among several new cool features, JUnit5 has come with new annotations (e.g @BeforeAll, @AfterAll, @ExtendWith), JDK 8 Lambda support to let us write more compact tests, and extensions. We will look into some of the new features later in the tutorial.

To add the JUnit5 dependancy add the following to your pom.xml file.

Fun fact : JUnit 5 is nicknamed Jupiter since it’s the 5th planet from the Sun.

H2 database

H2 is an open-source lightweight in memory Java database. It can be embedded in Java applications or run in the client-server mode. H2 is a good choice because it is written in Java so it integrates well with Java applications, it is fast thus allowing our tests to run faster and it is open source. We will see H2 in action when will be writing our Component tests.

We include H2 database in our application by adding the following to our pom.xml file:

Fun Fact: The name H2 stands for Hypersonic 2 for its speed.

Mockito

An open source testing framework used for creation of mock objects. Mock objects are objects that mimic the behavior of real objects in controlled ways. We write mock objects so that we can focus on the behavior of the system or subsystem we are testing without worrying about its dependencies. Mockito facilitates creating mock objects seamlessly. It has simple to understand syntax as we will see later when writing unit tests. Mockito also integrates well with the Spring framework i.e we can use them together without any conflicts.

The spring-boot-starter-data-rest dependancy already adds Mockito to our application.

JsonPath

JsonPath is a query language for JSON that lets you extract the bits of JSON document our application needs. JSONPath defines expressions to traverse through a JSON document to reach to a subset of the JSON. We will use JsonPath in our assertions when we will be writing tests that verifies responses from our REST APIs. We would like to check if the JSON data received matches what we expect i.e format and content.

The spring-boot-starter-data-rest dependancy already adds JsonPath to our application.

AssertJ

AssertJ is an assertion library used to write our assert statements. An assert statement tests a predicate if its true or false based on the output of program. AssertJ greatly improves the readability of assert statements. The new version of AssertJ has a rich set of assertions, allows us to write assertions that are much closer to natural language and enables the chaining of multiple assertions which improves test code readability and makes maintenance of tests easier.

The spring-boot-starter-data-rest dependancy already adds AssertJ to our application.

Hamcrest

Just like AssertJ, Hamcrest is also an assertion framework for writing matcher objects. It allows us to define ‘match’ rules declaratively. We can match almost everything using Hamcrest: Lists, Collections, Objects etc. The difference with AssertJ is that we can use Hamcrest matchers in MockMVC methods. With AssertJ we need to extract the HTTP response body first and do the assertions therefore we can write less code using Hamcrest. Generally speaking, you may choose whatever assertion framework you like when testing and this tutorial is an oppurtunity to learn both.

The spring-boot-starter-data-rest dependancy already adds HamCrest to our application.

Fun Fact : Hamcrest is essentially an anagram for Matchers.

WireMock

Is a tool that can mimic the behavior of an HTTP API and capture the HTTP requests sent to that API. This allows us to stay productive when an API we depend on doesn’t exist or isn’t complete. It also comes in handy when we are writing integration tests as we will see later.

Add the following dependancy to pom.xml file.

Testcontainers

From the Testcontainers documentation: Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. We will use Testcontainers in persistence integration testing. Testcontainers allow us to use a containerized instance of a MySQL, PostgreSQL or Oracle database, but without requiring complex setup on your machine. Any other database type that can be containerized can also be used without installation on your machine. All you need is Docker.

Add the following dependancy to your pom.xml file. We will be using MySQL in our application.

More Spring Framework concepts

In this section we will give brief information of some of the concepts of the Spring Framework to ensure the reader has better understanding.

ApplicationContext

The ApplicationContext is the central interface within a Spring application that is used for providing configuration information to the application. It basically provides the configuration needed by our application e.g Bean factory methods for accessing application components. When running our tests we care alot about the ApplicationContext. Different tests require different application contexts as we will see later.

Beans

In Spring, the objects that form the backbone of the application and that are managed by the Spring IoC container are called Beans. A Bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container. You may be wondering what is IoC? Simply put, Inversion of Control, or IoC for short, is a process in which an object defines its dependencies without creating them. This object delegates the job of constructing such dependencies to an IoC container.

Why are we concerned with Beans? In writing tests, we need to clearly understand what are the dependancies (or the Beans) of our test classes and know how to inject them to our test class.

Dependency Injection

The process by which an object receives other objects it depends on as its input is known as dependency injection. It is a concept frequently used in mocking and one of the primary reasons behind why we mock our objects to create these dependencies while testing.

Controller

In Spring MVC, a Controller is a class with the following responsibilities

  • Intercepts incoming requests
  • Converts the payload of the request to the internal structure of the data
  • Sends the data to Model for further processing
  • Gets processed data from the Model and advances that data to the View for rendering

Spring Profiles

Spring provides a clever way for grouping configuration properties into so-called Profiles. This allows us to activate a bunch of configurations with a single profile parameter. Understanding how to create profiles is important when writing tests because different tests require different configurations as we will see. We will learn how to create a profile later.

Running tests from command line

To run tests for this tutorial from the command line follow the following steps:

1. Open command line and do git clone https://github.com/gibchikafa/springbootmicroservices.git in some directory e.g Desktop, Documents or any other folder

2. cd springbootmicroservices

4. Open another command line window in the springbootmicroservices folder

5. cd cleaningschedule. This is the directory that contains our cleaning schedule microservice.

6. Run the following command: mvn test. This will run all our tests we have written for our cleaningschedule microservice. If you would like to run just a single test class use: mvn -Dtest=ClassName test, e.g mvn -Dtest=CleaningScheduleComponentTest test will run all tests in the CleaningScheduleComponentTest class. To run multiple classes use: mvn -Dtest=ClassName1,ClassName2,ClassName3 test. If you would like to run just a single test method from a class use: mvn -Dtest=ClassName#methodname test, e.g mvn -Dtest=CleaningScheduleComponentTest#getWeekCleanersTest test to run the getWeekCleanersTest() test method.

7. Open another command line window in the springbootmicroservices folder

8. cd accommodation

9. Run the following command: mvn test. This will run all our tests we have written for our accommodation microservice.

Persistence Integration tests will require Docker to create a contenerized instance of MySQL.

Wow, that was a lot of background to cover 😅 😅. We hope that its given you a greater appreciation for what we are trying to achieve in this tutorial.

Now without further ado, let us jump right into the tutorial!!

Unit Testing

Unit tests on the Service Layer

To do unit testing on the service layer we have to test each function implemented in the service layer. Remember in unit testing, we only want test only that piece of code minus all the other dependencies. Here is a snippet of code and below we explain in detail what exactly we are doing:

Note: The numbers below are in reference to the numbers used in the snippet above, we have used this to explain the blocks of code in detail. This is just one of the unit tests we wrote on our microservices.

  1. One of the new annotations in JUnit5 is @ExtendWith. It is a declarative way to register a custom extension on a test class or method. It tells Jupiter’s test engine to invoke the custom extension for the given class or method. We would like to use Mockito in our test so we provide an MockitoExtension as an argument. This allows us to use the other Mockito annotations inside the class. Without this our mocks can not work.
  2. @Mock is a Mockito annotation used to create and inject mocked instances or dependency classes. We are mocking ScheduleRepository and AccomodationServiceProxy classes. Put this annotation on the class or method you would like to mock.
  3. @InjectMocks creates an instance of the class that we are testing and injects the mocks that are created with the @Mock annotation into this instance. Put this annotation on the class you would like to test.
  4. In order for the method to be recognized as a test we add the @Test annotation. Make sure you use org.junit.jupiter.api.Test from JUnit5.
  5. Since it’s just a Mockito-backed mock, we use Mockito to pre-program the stub so that it will return the pre-programmed responses. We use this on the classes we annotated with @Mock.
  6. Here we write our assertion that will tell us if our test passes or not. In order to write an assertion, you always need to start by passing your object to the Assertions.assertThat() , from AssertJ library, method and then you follow with the actual assertions.AssertJ library

We have seen how to do a unit test on some service layer function. You can run the ScheduleServiceTest class using the following command: mvn -Dtest=ScheduleServiceTest test. To run the getCleanersForWeek() test method:mvn -Dtest=ScheduleServiceTest#getCleanersForWeek test. Next we look at how to do a unit test on the controller class.

Unit tests on the Rest Controller

The goal of testing the controller is to verify if we can reach the declared endpoint and that it returns the correct response. We are testing the controller in isolation regardless of the business logic. There are some differences from the previous unit test on the Service layer especially in terms of annotations as we will see. Here is an snippet of code from our demo project that shows how to do the test:

  1. We would like to use Spring test framework features in our tests. It creates application context that allows us to send HTTP requests to our system thus we are able to test our Spring MVC controllers. To be able to do this we extend our test class with SpringExtension. In our previous test we did not require to send any HTTP request so we did not use this extension.
  2. We use the @WebMvcTest annotation to fire up an application context that contains only the beans needed for testing a web controller.
  3. We use the @MockBean annotation on the ScheduleService to mock away the business logic as we are not interested in the business logic, remember the goal for testing our controller we highlighted at the beginning of this section. It allows to add Mockito mocks in a Spring ApplicationContext or in other words it automatically replaces the bean of the same type in the application context with a Mockito mock.
  4. Tell Spring to create a WebMvc instance
  5. @BeforeAll is another new feature in JUnit5. It tells the runner to execute this method before all the tests. Use this if you want to do something before all the tests are run e.g initialization. In our case we want to create a defaultSchedule that will be used in all the test methods.
  6. Here is where we do our verification. To verify that a controller listens to a our HTTP request we simply call the perform() method of MockMvc and provide the URL we want to test. We also verify that the correct HTTP method (GET in our case) and the correct request content type.
  7. We now start to verify the response from the server. We use the andExpect method and provide the result matcher. The status().ok(), from the MockMvcResultHandlers, verifies that the HTPP response code is 200.
  8. We verify the body of the response. jsonpath evaluates the given JsonPath expression against the response body and assert the resulting value with the given Hamcrest Matcher. This is where Hamcrest comes in. Hamcrest comes with several matchers we can use to verify our JSON in the response body. hasSize verifies that the size of the JSON in the response body is equal to the size of our array. is verifies that the two values are equal. You can compare about all data types with is e.g lists, class instances etc. As you can see Hamcrest matchers are declarative.

We have introduced unit testing for service layer and the controller. Additionally we have described the basic annotations you can use, mock dependencies, and perform assertions among others.

Unit testing alone does not provide guarantee about the system behavior in most cases. So far we have good coverage of each of the core modules of the system in isolation. However, there is no coverage of those modules when they work together to form a complete service.

To verify that each module correctly interacts with other module in the microservice, more coarse grained testing is required and now we switch to component testing for the same.

Component Testing

Testing the whole component without other third-party code and services.

Remember that the goal of componet testing is to test that different parts of the service (controller, service layer, and repository) work together isolating third-party code and services. Isolation of the service is achieved by replacing external services with mocks and in memory database for external datastore.

For our demo, we are just going to mock our accommodation service using Mockito. In integration test we will see another way we can achieve this.

To use an in memory database we should use a different configuration for our component tests. In Spring we create a different profile by putting the configuration in separate application-<profile>.properties. We are going to use H2 in memory database.

Above is the application-componenttest.properties. In our component test class we are going to use this profile.

  1. Enables all auto-configuration related to MockMvc. Our code will be called in exactly the same way as if it were processing a real HTTP request but without the cost of starting the server.
  2. Loads our whole application, creating all our components. Note the use of webEnvironment=RANDOM_PORT to start the server with a random port (useful to avoid conflicts in test environments).
  3. Use the profile we created. When running this test we are going to use the configuration we created.
  4. This is for the configuration for the lifecycle of JUnit 5 tests. LifeCycle.PER_CLASS enables us to ask JUnit to create only one instance of the test class and reuse it between tests. We intend to save data to the database only once in the init function annotated with @BeforeAll.
  5. We mock our external service to isolate our service.

Notice that we do not mock our repository interface, service or controllers to achieve our goal. We are using real objects so that we verify the whole functionality of our microservice i.e all our system componets can really communicate.

Integration Testing

Gateway integration testing

We do not necessarily need to have the other service running. What if it is not yet developed? To achieve this we use WireMock, a simulator for HTTP-based APIs. When our FeignClient interface, implemented in AccomodationServiceProxy.java, calls the accommodation service, our WireMock server will be responsible for handling the request.

  1. We are going to use the gatewayintegrationtest profile.
  2. WireMockServer instance.
  3. Configuration for our mock server. We use the same port used by the accommodation service. Before starting the tests we need to start the server.
  4. We create a stub which behaves like one of the controller methods. We can specify the attributes of an HTTP request e.g request method, request URL and response.
  5. @AfterEach is a JUnit5 annotation. The function will be executed after each test has completed.
  6. We want to free all resources that are reserved before a test method is run, we have to stop our WireMock server in a teardown method that is run after a test method has been run. We can stop our WireMock server by invoking the stop() method of the WireMockServer class.
  7. We perform a request to get a resident of the room which requires the accommodation service. When our gateway interface sends the request it will be handled by the WireMockServer. We can then perform assertions on the response.

Persistence integration test

We are going to use Testcontainers. Here is a configuration to be able to use a conterinerized instance of MySQL:

  1. We are going to use persistenceintegrationtest profile we created.
  2. We save our object to the database
  3. We perform a request to get the schedules. We then perform assertions to check if it returns the object we saved.

When you run the above test you will see that a Docker container of MySQL is created and provided for our application. The container is then destroyed after the test finishes. Below is log to show that the container is indeed created.

Running the tests in Eclipse

You can also run all the tests in Eclipse by following the following steps:

  1. Navigate to src/test/java and right click.
  2. On the menu choose Run as, the click JUnit.

This will run all the tests we have written. JUnit runner will give an output like this:

It shows the total test run, errors and failures. The green bar shows that all our test were successful! Exactly what we want!!

Next Steps

This tutorial focused on writing tests and running the tests on the local machine. The next step is to make a Continous Integration (CI) pipeline so that our tests are automatically executed everytime we push our code to our repository master branch e.g Git. There are several platforms available to create CI pipeline for Spring Boot application together with your repository e.g Jenkins, Azure DevOps, Gitlab etc. Explore one of these platforms and create a CI pipeline to automate the running of the tests.

Conclusion

In this tutorial we have covered how to write unit, integration and component testing for microservices. By combining unit, integration and component testing, we are able to achieve high coverage of the modules that make up a microservice and can be sure that the microservice correctly implements the required business logic. However, there are other higher level tests not covered in this tutorial e.g contract tests. By following this tutorial we hope that the readers will be able to write tests on microservices.

We hope you found this tutorial as interesting as we did in coming up with it. Please post your responses on the same and do leave a few claps if you liked it, it really motivates us to keep venturing on new ideas!!

--

--