Overview

Integration testing strategies for Spring Boot microservices part 2

No Comments

This is the second part of my earlier post about strategies for integration-testing Spring Boot applications that consist of multiple (rest) services.

You can find the accompanying sample application in my gitlab account:

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

In my previous post I pointed out that unit tests are great for checking code correctness — while not actually proving it, but let’s not go there. They are also an essential tool to safeguard coding standards: if you can’t test it in a unit test, it’s probably badly designed.

However, the tool stacks used to build today’s server applications typically follow a convention-over-configuration approach, where you can leave out much of the boilerplate configuration in favour of a sensible default. This saves you the time and effort to gain in-depth knowledge of the framework yet makes it all the more important to check that the framework is behaving as expected. Fortunately Spring also supplies you with the tools to do just that and we looked at some integration tests that make a Spring context available for testing purposes.

Spring integration tests are great to check if our software uses the tools/middleware correctly: although they can run alongside regular unit tests, the important difference is that the scope of our test is no longer a unit of code isolated from the rest by means of mocking, but an entire server instance.

Advantages of using Spring Unit test:

  • Easy to set up: @RunWith(SpringRunner.class)
  • Quick to run
  • very flexible: inject dependencies to manipulate input and validate state

Drawbacks:

  • Not really a faithful representation of the actual live runtime.
  • Tests only a single spring context, not the interaction between multiple services

Let’s return to our weather server. Remember that we’re building a central REST server that resolves a Dutch postcode to a location of a network-enabled weather station for up-to-date readings. Suppose that the Java part of our application landscape is nearing completion. The only thing missing is the code to read out the physical devices, but as the Lead Systems Architect you have given that team the required interfaces, so they can’t hold you back from running a test of the full system while mocking the actual reading of the devices with ThermometerAdapter and HygrometerAdapter and set a fixed return value through application properties at startup.

@Service
@Profile(“test”)
public class StubThermometer implements Thermometer {
 
    @Value("${temperature:42}")
    private double temperature;
 
    public double getTemperatureInCelsius() {
        return temperature;
    }
}

At a later date you can get rid of the stubs, or keep them around, annotated with @Profile(“test”), and annotate the ‘actual’ thermometer with @Profile(“!test”). Purists will argue that you should never have anything intended for testing purposes mixed with your production code, but I disagree. The mocks are not a temporary kludge but the only way to to run a repeatable automated test of our system.

In this stage of our integration test we want the multiple hosts to run in separate JVMs, with all interaction between them going through the network.
This is a step up from the @SpringRunner integration test towards the eventual live situation, though bear in mind that it is still not a true end-to-end test: all server instances will run on the same metal instead of on separate IoT-machines over the country, and the physical measuring devices are still stubbed.
For this test I have created a separate maven project atdd (acceptance-test driven development) that has no source dependencies on either the weather server or weather station projects. For convenience it depends on the weatherapi project, but this is only to use the data transfer objects (dtos) returned from and received by the weather server rest endpoints.

The atdd tests does basically the same as the weather server Spring integration test in terms of queries and validation but with the crucial difference that the servers run on separate TCP ports and that the framework will orchestrate the start-up and shutdown of all service, and it will do so with the packaged jar artefacts. I have chosen the Cucumber test framework to write these test scenarios in a more readable format using the gherkin DSL:

Feature: Get hard-coded temperature for various postal codes
 
  Scenario: get temperature for correct postal codes in celsius
    When I get the temperature for postal code 2000 in celsius
    Then the temperature is -3.5 in celsius
    When I get the temperature for postal code 4000 in celsius
    Then the temperature is 2 in celsius
    When I get the temperature for postal code 7000 in celsius
    Then the temperature is 6.3 in celsius
 
  Scenario: get temperature for correct postal codes in fahrenheit
    When I get the temperature for postal code 2000 in fahrenheit
    Then the temperature is 25.7 in fahrenheit
    When I get the temperature for postal code 4000 in fahrenheit
    Then the temperature is 35.6 in fahrenheit
    When I get the temperature for postal code 7000 in fahrenheit
    Then the temperature is 43.3 in fahrenheit
 
  Scenario: get humidity for correct postal codes
    When I get the temperature for postal code 2000 in celsius
    Then the humidity is 30 per cent
    When I get the temperature for postal code 4000 in celsius
    Then the humidity is 35 per cent
    When I get the temperature for postal code 7000 in celsius
    Then the humidity is 40 per cent

The text of these so-called gherkin glue lines can contain placeholders for temperature and post code values. TemperatureRetrievalSteps contains the Java meat to do the actual rest calls, using the same RestTemplate we came across earlier.

public class TemperatureRetrievalSteps {
    private RestTemplate client = new RestTemplate();
    private ResponseEntity response = null;

    @When("^I get the temperature for postal code (\\d+) in (celsius|fahrenheit)")
    public void getTemperature(String postCode, String unit) {
        String url = String.format("http://localhost:%d/weather?postCode=%s&unit=%s", ServerEnvironment.SERVER_PORT, postCode, getUnitCode(unit));
        response = client.getForEntity(url, WeatherReportDTO.class);
    }

    @Then("^the temperature is (.*?) in (celsius|fahrenheit)$")
    public void theTemperatureIs(double temperature, String unit) {
        assertCorrectResponse("Did not retrieve temperature: ");
        assertThat(response.getBody().getTemperature()).as("temperature").isEqualTo(temperature, Offset.offset(0.1));
        assertThat(response.getBody().getUnit().name()).as("temperature unit").isEqualTo(getUnitCode(unit));
    }
    //rest of content omitted
}

Very nice; but before running the cucumber scenarios we need to set up an environment of running servers. Let’s look at two very different ways to accomplish this, each with its own drawbacks and advantages. The first is to build a fully executable file and register it as a (Unix) service. You can then use start, stop, status commands like any other Unix service by means of the standard ProcessBuilder class. There is a similar way to do this for Windows, but it should be clear that this is not the most portable way. It requires extra privileges to register a service which is maybe not at all what you want for testing purposes.
A pure Spring solution is to use the actuator project, which among many other features supplies methods to monitor and shut down services over the network. This is the approach I will follow here. We still need a system call to start the application, but after that we can monitor and shutdown the instance through the web api.

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>

By default the shutdown hook is not supported, for obvious reasons. The following configuration will also disable security for our test purposes.

--endpoints.shutdown.sensitive=false --endpoints.shutdown.enabled=true --management.context-path=/manage

Needless to say you should not put this in the application.properties files of your production code. Better even still to use a build profile that excludes the actuator project entirely for the production build.

The ServerEnvironment constructs the start-up command that contains all the necessary hard-coded application properties for our test.

private static SpringApplicationWrapper createWeatherServerInstance(List<String> defaultArgs) {
     String jarFilePath = PathUtil.getJarfile(PathUtil.getProjectRoot() + "/springboot-testing-tips/weatherserver/target");
     defaultArgs.add("--server.port=" + SERVER_PORT);
     defaultArgs.add("--weather.host.1000-3000=http://localhost:" + STATION_1_PORT + "/weather");
     defaultArgs.add("--weather.host.3001-6000=http://localhost:" + STATION_2_PORT + "/weather");
     defaultArgs.add("--weather.host.6001-9999=http://localhost:" + STATION_3_PORT + "/weather");
     return new SpringApplicationWrapper("http://localhost:8090/manage", jarFilePath, defaultArgs);
  }

The SpringApplicationWrapper stands for a single server instance and is responsible for invoking the java -jar executable_file.jar command and issuing the /mappings and /shutdown calls.
All that’s left is to start-up and shutdown the environment using the @BeforeClass and @AfterClass hooks in JUnit. Note that cucumber runs each scenario as if it were a separate JUnit test class. If we had annotated the setup/teardown hooks with @Before and @After they would have been invoked for each scenario. For performance reasons we want to start-up and shutdown the environment only once, so we have to use static method calls, which is less pretty but unavoidable.

@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:features", format = {"pretty", "json:target/cucumber-html-reports/testresults.json"})
public class RunCucumberTest {
    @BeforeClass
    public static void setup() {
        ServerEnvironment.start();
    }
 
    @AfterClass
    public static void tearDown() {
        ServerEnvironment.shutdown();
    }
}

And there’s more…

Once the UI is in place we can integrate it in this project and initiate a browser session with Selenium to manipulate the UI controls. The control over our running servers allows you even to run recovery scenarios by stopping and starting services and ensure that the other nodes react appropriately. Remember that caching the readings from the weather stations is an important task of the weather server, since bandwidth is limited and too frequent readings make no sense. You could programmatically stop one of the weatherstation instances, do a request to the weatherserver, which will serve the response from cache and not even notice the station has gone offline, provided it is back up before the cache timeout expires. All this you can test with the kind of setup I outlined. There is somewhat more boilerplate involved than with a regular integration test but you can extract much of it to a separate testing library and re-use it for new atdd projects. I wish you happy testing.

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 *