Overview

Integration testing strategies for Spring Boot microservices

No Comments

SUMMARY: Unit tests are a necessary condition to clean code, but today’s convention-over-configuration frameworks like Spring Boot are often used to build applications consisting of multiple services. You need some way of ensuring that the parts are going to fit together and that you are using the framework properly.

Since this is a blog for serious people who are serious about software I should not have to explain the virtues and importance of solid automated tests but I will do so anyway. Why should we test, other than for the obvious acknowledgement that we are mere fallible mortals?

  • Tests ensure that changes somewhere in the code do not cause unexpected behaviour elsewhere.
  • They validate that the code behaves as designed.
  • They safeguard good coding principles.
  • They make sure the various pieces of the product fit together

In short: tests lessen the risk that your product is going to make the customer angry or unhappy.
There are many methods to test and not all aims listed above apply equally (or at all) to each method at our disposal.

Unit tests: solitary and limited by design

In test-driven design (TDD) writing test assumptions should precede writing production code. A most noble principle, although in practice it’s likely to proceed more hand in hand. A key benefit however of working this way is that it strengthens your understanding of the code under construction, invites you to seek out edge cases and protects you against yourself by making it hard to cut corners since badly designed code is often hard, if not impossible to test. A good unit test is the canary in the coal mine of evil coding. Coupled with good mutation testing coverage, it’s an invaluable tool to ensure that any change which makes your software produce different output will result in a failed test.

The canonical purpose of a unit test is to focus rigidly on a single piece of code, with every invocation of code external to the unit mocked out, preferably by a mocking framework like mockito or jmockit. Abstracting away all dependencies may be the correct way to do unit testing, it also means that these tests in no way guarantee that the product as a whole works as designed. That’s okay: they’re a necessary though not sufficient condition to code quality.

Modern day container environments like Spring Boot can be set up in a convention-over-configuration fashion providing a fully fledged server application with JPA persistence, web security and JSON serialisation, all with minimal boilerplate code. That’s great, but handing off all these responsibilities to the framework makes it even more important to check that you’re using the framework properly, none of which can be guaranteed with a unit test. A case in point:

@OneToMany(mappedBy="paremt")
private List children

The spelling mistake in the mappedBy property will cause a runtime error in the persistency framework

@RequestMapping(value="/my/unique/path", method = RequestMethod.GET)

If another method in another rest controller tries to use the same /my/unique/path with the GET http verb, the Spring context will fail to start up.

I could go on for a while…

The problem with end-to-end testing

We should test our application as it is actually going to be run in production, with none of the dependencies mocked or stubbed. Fair enough, but in a typical web application this means serving a front-end and back-end, setting up and populating a database, not to mention a whole host of other services external to your team, either already existing or being developed elsewhere. A typical end-to-end test for a web shop would fire up a browser, complete an order in the form (e.g. with Selenium) hit the order button and check that the payment server, warehouse and courier are informed appropriately. Such tests are expensive both in terms of setting up and running.
What’s more, they’re impossible to build from day one: in a large, distributed team components will not be developed at the same pace. There may not be a ‘PLACE ORDER’ button to click on for months after the back-end team already finished their REST endpoint.

The weather server

Imagine we are building the backend of a shiny new weather app that gives up-to-date meteorological information from a number — say thirty — weather stations in the Netherlands (observant readers will notice that I used the weather metaphor already in my post about caching). Users can send a city, a postal code or a GPS coordinate through their mobile device to the central server which delegates the request to the appropriate weather station and returns it to the user. Results are cached to prevent querying the weather station again within the same minute.
Your mission, should you choose to accept, is to develop this weather server. For now we’ll only support four-digit postal codes and only three weather stations. Another team of more hardware-inclined geeks will hook up a thermometer and hygrometer to an Arduino with 4G connectivity which will listen to REST requests and send accurate temperature and humidity data. Yet another team of bearded JavaScript hipsters in Amsterdam will build the front-end, but they’re out of the equation entirely for now.
An automated end-to-end test is not going to happen, since the measurement of environmental data is beyond the scope of a software test. Anyway, the weather station device is nowhere near completion. We do however like to test our weather server beyond the simple unit test.

Integration testing with SpringRunner

With integration testing in Spring you can step beyond the shortcomings of the unit test long before the components of the product are ready for an end-to-end test. In fact, integration tests can and should go hand in hand with unit tests.

Download the sample project here:

git clone git@gitlab.com:jsprengers/springboot-testing-tips.git

The project consists of four sub-projects. Weatherstation is the implementation of the net-enabled thermometer and atdd contains Cucumber tests. They will be covered in the part two of this post, on end-to-end testing.

The api project contains the WeatherReportDTO transfer object and the interface to be implemented by the weaterstation. Actually, this being a RESTful service, the weatherstation doesn’t have to be developed with Spring or even Java, but let’s assume that it is. There are other ways to cast a REST api in concrete, but please let’s not go there. Having a separate api project makes it possible for the weaterstation developers to import the specification without any reference to the weatherserver project, and that’s a good thing.

The server will communicate with three weather stations by mapping a postal code to the url for the appropriate station. Since it’s a fixed and limited number we inject the urls as separate values, which can be fed as application variables upon startup or be read from file — if you choose the latter option then keep it out of your packaged .jar file, so a change in url doesn’t require a rebuild! In addition it can return the temperature in degrees Fahrenheit for visiting Americans. The weather stations always return degrees Celsius, this being Europe.

@Value("${weather.host.1000-3000}")
private String host1000_3000;
@Value("${weather.host.3001-6000}")
private String host3001_6000;
@Value("${weather.host.6001-9999}")
private String host6001_9999;

Notice that a unit test would be sadly inadequate: it cannot check that the proper values are injected at runtime, it cannot check that there’s another rest controller mapped to the same /weather url and least of all it cannot check that the rest client actually connects to the weatherstation server and retrieves a correct result. All that can be done in Spring test. The actual @Test methods are omitted for brevity; you can find them in the source code.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {
        WeatherServerApplication.class,
        NorthWeatherStation.class, SouthWeatherStation.class},
        webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@TestPropertySource(properties = {
        "weather.host.1000-3000=http://localhost:8090/north/weather",
        "weather.host.3001-6000=http://localhost:8090/south/weather",
        "weather.host.6001-9999=http://localhost:8090/south/weather"})
public class WeatherServerIntegrationTest {
 
    private RestTemplate restTemplate = new RestTemplate();
 
    private void assertWeatherForPostcode(String postcode, double temperature, String unit, int humidity) {
        String url = String.format("http://localhost:8090/weather?postCode=%s&unit=%s", postcode, unit);
        WeatherReportDTO temperatureObject = restTemplate.getForObject(url, WeatherReportDTO.class);
        assertThat(temperatureObject.getTemperature()).isEqualTo(temperature);
        assertThat(temperatureObject.getUnit().name()).isEqualTo(unit);
        assertThat(temperatureObject.getHumidity()).isEqualTo(humidity);
    }
}

Our WeatherServerIntegrationTest runs as any other JUnit test, but it fires up your Spring Boot application and turns your test class into a component where you can inject any component or service with the @Autowired annotation. The @SpringBootTest classes property points to our production application WeatherServerApplication and you can add any number of test-specific managed components or other @Configuration classes. Bear with me and I’ll get to the NorthWeatherStation.
We could inject our WeatherServerEndpoint and invoke its getTemperatureForPostalCode:

@Autowired
WeatherStationRestClient client;
 
@Test
public void getTemperatureForPostalCodeInFahrenheitShouldBe42(){
   WeatherReportDTO report = client.getTemperatureByPostalCode("1234","F");
}

But since SpringRunner has started an actual REST server why not query the endpoint straight away, using the RestTemplate, a useful wrapper around the http client and JSON serialisation bureaucracy.

Not so fast though: given a valid postal code, our server will want to query another REST endpoint, namely the one that our charming but reclusive IoT friends are soldering together. But that isn’t there yet. There are two ways to go about it. The first is to use a mock REST client in our sever. We could add a @Profile(“!test”) annotation to our WeatherStationRestClient class, add a test implementation annotated with @Profile(“test”) to our Spring test config and the context would instantiate our mock implementation instead of the production one.

@Service
@Profile("test")
public class StubWeatherStationClient implements WeatherStationClient {
  WeatherReportDTO getTemperatureByPostalCode(int postCode, String unit){
  return new WeatherReportDTO(20,TemperatureUnit.C,60);
}
}

I don’t like this solution, because it cuts out the juicy, error-prone REST call to the weather station. That’s  a missed opportunity. Besides, it requires you to annotate the production implementation with a @Profile(“!test”) and to run your test with the “test” profile. Better to leave the production code untouched and let it connect to an actual weather station endpoint through REST, albeit one of our own making and only available to our integration test. Writing a rest endpoint that gives back a hard-coded value for test purposes is as easy as:

@RestController
@RequestMapping("north/weather")
public class NorthWeatherStation implements WeatherStation {
 
    @RequestMapping(method = RequestMethod.GET)
    public WeatherReportDTO getWeatherReport() {
        return new WeatherReportDTO(-3.2, TemperatureUnit.C, 30);
    }
}

To let the SpringRunner context manage that controller, just add it the the classes property of @SpringBootTest and the test context will host both the weather server and our two weather station stubs.

We’re not quite there yet. We have to point our WeatherStationRestClient  to the correct test endpoint, i.e. the one we just supplied. There are three URLs it can connect to, depending on the postal code provided. In the live situation these will be different hosts listening to the same path. For our test setup let’s create two different implementations (north and south) and map codes 1000-3000 to the first, and all other codes to the second. To make this happen the endpoints will listen to different paths (running on the same host), but that’s okay since the weather server makes no assumption about urls anyhow. All can be configured through application properties, and you can supply them in an application.properties file in src/test/resources or for a one-off test case through the TestPropertySource annotation.

@TestPropertySource(properties = {
        "weather.host.1000-3000=http://localhost:8090/north/weather",
        "weather.host.3001-6000=http://localhost:8090/south/weather",
        "weather.host.6001-9999=http://localhost:8090/south/weather"}

With little set-up code and a few concise service stubs we now have a fully fledged integration test that not only covers all of our application logic (mapping postal codes and handling Celsius to Fahrenheit conversion) but also tests tests that our REST server and weather station client actually work.

In the next part I will go one step further and show you some strategies to manage startup and shutdown of several packaged Springboot applications in a test environment using the Cucumber framework. This setup contains no more stubbing or mocking and comes close to an actual end-to-end test.

Jasper joined codecentric NL in 2015 but has been coding since the early eighties. Having a background in English and linguistics, he always likes to stress the importance of human language in software.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Comment

Your email address will not be published. Required fields are marked *