Sylvain Lemoine
Another dev blog in the nexus…

Spring Boot Test Howto

· by Sylvain Lemoine · Read in about 13 min · (2563 Words)
Java Spring Framework Spring Boot Spring Security Tests

This article aims to be an howto guide for developers who want to test their Spring Boot applications. It deals with some «real life» projects use cases which might be useful to you.

As always, sources are available on my github: https://github.com/slem1/spring-boot-test-howto. Besides, the end of each how-to section contains a «Follow the commit» link to the corresponding commit in the github sources.

How to setup tests in Spring Boot project

There is almost nothing to setup. Let’s assume a simple project. One maven module, spring-boot-test-howto and one submodule spring-boot-test-howto-webapp.

+-------------------------------------+
| spring-boot-test-howto              |
| +---------------------------------+ |
| | spring-boot-test-howto-webapp   | |
| |                                 | |
| +---------------------------------+ |
+-------------------------------------+

spring-boot-test-howto parent module pom.xml

    <dependencyManagement>
        <dependencies>
            <dependency>
                <!-- Import dependency management from Spring Boot -->
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>1.5.7.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <version>1.5.7.RELEASE</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>build-info</goal>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

spring-boot-test-howto-webapp submodule pom.xml

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

just add the spring-boot-starter-test dependency to the spring-boot-test-howto-webapp module.

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

Create a test

The root package of all my class will be fr.sle.testhowto

Create class fr.sle.testhowto.test.MyTestClass in the test sources directory.

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyTestClass {

     @Test
     public void aTestShouldAssertSomething(){

     }
}

Run it and… failure !

java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...)

Follow the commit

Ok, So @SpringBootTest looks for @SpringBootConfiguration annotated class as the starting point for loading the spring configuration from the sources in the following order: - fr.sle.testhowto.test - fr.sle.testhowto - fr.sle - fr dead end !

A webapp is runnable afterall, so there is no point to create an app without entrypoint.

Let’s give spring boot what it wants, and add the spring boot entry point of the app in the main sources.

package fr.sle.testhowto;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String... args){
        SpringApplication.run(Application.class);
    }
}

This is where having a clear and standard naming convention is important.

If I had named the testing package test.fr.sle.testhowto, I would have break the out-of-the box behavior of Spring Boot. Spring Boot would not have discovered the Application class in fr.sle.testhowto package because it would have scanned the following packages:

  • test.fr.sle.testhowto
  • test.fr.sle
  • test.fr
  • test

I always try to keep things straightforward as much as possible, so keeping a common root hierarchy between app source code and test source code package is the way I go.

Re-run the test, it should be ok. Spring boot will now load your Spring Application Configuration from the starting point of fr.sle.testhowto.

Follow the commit

How to use @SpringBootTest on my library submodule

What if you have a java library shared between multiple maven runnable module ?

+----------------------------------------------------------------------------+
| spring-boot-test-howto              										                   |
| +---------------------------------+    +---------------------------------+ |
| | spring-boot-test-howto-webapp   |    | spring-boot-test-howto-business | |
| |                                 | 	 |                                 | |
| +---------------------------------+ 	 +---------------------------------+ |
+----------------------------------------------------------------------------+

In that case it is a nonsense to had an @SpringBootApplication annotated entry point to the main sources of spring-boot-test-howto-business submodule.

Anyway if you keep the package naming convention as explained above there is no problem to rather add the @SpringBootApplication annotated class in the test sources into the root package fr.sle.testhowto

test sources classes

package fr.sle.testhowto;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ApplicationTest {
}

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyComponentTestClass {

    @Autowired
    private MyComponent component;

    @Test
    public void myComponentTestMethod(){

    }
}

main sources classes

package fr.sle.testhowto;

import org.springframework.stereotype.Component;

@Component
public class MyComponent {
}

myComponentTestMethod test method should run normally.

Follow the commit

How to add additional Spring test configuration

Sometimes you need to add extra Spring configuration (additional beans for instance), or alter the existing configuration with mocked bean instances.

You can use an annotated @TestConfiguration class for that. You’ll have to point out the additional configuration class in the @SpringBootTest annotation.

test sources classes

package fr.sle.testhowto.test.config;

/**
 * Additional component class for test
 */
public class AdditionnalTestComponent {
}

The additional config with one extra component, and a mock of a MyComponent bean:

@TestConfiguration
public class AdditionalTestConfig {

    @Bean
    public AdditionnalTestComponent additionnalTestComponent() {
        return new AdditionnalTestComponent();
    }

    /**
     * A Mock if {@link MyComponent} class
     * @return the mock
     */
    @Bean
    public MyComponent component() {
        return Mockito.mock(MyComponent.class);
    }
}

The test configuration

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AdditionalTestConfig.class)
public class MyComponentTestClass {
  /** code **/
}

Be careful to not use @Configuration instead of @TestConfiguration

@Configuration breaks the spring boot autoconfiguration mecanism. For instance, with the following AdditionalTestConfig:

@Configuration
public class AdditionalTestConfig {

    @Bean
    public AdditionnalTestComponent additionnalTestComponent() {
        return new AdditionnalTestComponent();
    }
}

you’ll get

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'fr.sle.testhowto.MyComponent'

default component scanning left the party here. Let’s bring it back:

Switch of @Configuration for @TestConfiguration

@TestConfiguration
public class AdditionalTestConfig {

    @Bean
    public AdditionnalTestComponent additionnalTestComponent() {
        return new AdditionnalTestComponent();
    }
}

It should be ok now, your main sources configuration will be scanned and beans available at runtime.

Follow the commit

How to run database integrated test

Add the data-jpa starter to your model layer module (e.g. spring-boot-test-howto-business module)

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

and any entity

@Entity
@Table(name = "my_entity")
public class MyEntity {

    @Id
    @GeneratedValue
    @Column(name = "id")
    private Long id;

    @Column(name = "value")
    private String value;
}

Entities should be out-of-the box discovered by Spring Boot auto config.

How to run in-memory database test

If you want to run in-memory database, add a supported in-memory database driver to your test dependencies (h2, hsql)

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.196</version>
    <scope>test</scope>
</dependency>
2017-10-24 13:22:32.567  INFO 8372 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2017-10-24 13:22:32.575  INFO 8372 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2017-10-24 13:22:32.610  INFO 8372 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2017-10-24 13:22:32.951  INFO 8372 --- [       Thread-3] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete

Test runs through automatic schema export operated by Hibernate on the in-memory database.

Follow the commit

How to run real database test

If for any reason, you need to run a test on a «real» database, just add your datasource in the classpath.

create application.properties in test resources root

application.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/testhowto
spring.datasource.username=testhowto
spring.datasource.password=testhowto
spring.datasource.driver-class-name=org.postgresql.Driver

Follow the commit

How to switch datasource url between platform

If you use a continuous integration solution, you might need to switch datasource according to your build platform. For instance, on your computer you might have a local postgres database for running database integrated test, and another one when you build your app on Jenkins.

One way to handle this use case is combining Maven resources filtering and Spring boot profile.

How to set the spring boot profile

We keep our main application.properties in test resources root with subtle modifications:

spring.profiles.active=@spring.profiles.active@

spring.datasource.username=testhowto
spring.datasource.password=testhowto
spring.datasource.driver-class-name=org.postgresql.Driver

and add two additional profile specific properties files:

application-local.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/testhowto

application-ci.properties

spring.datasource.url=jdbc:postgresql://[MY_SERVER_IP]:5432/testhowto

If we give the local value to the spring.profiles.active property the datasource from application-local.properties will be loaded, and conversely with the ci value.

Follow the commit

How to filter the active profile value with maven.

By using maven test resources filtering.

Add the following properties and build configuration to your pom.xml

  <properties>
      <!-- default profile value -->
      <spring.profiles.active>local</spring.profiles.active>
  </properties>
    <build>
        <testResources>
            <testResource>
                <directory>src/test/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>**/*.properties</include>
                </includes>
            </testResource>
            <!-- /!\ Important - Copy non filtered resources too ! /!\ -->
            <testResource>
                <directory>src/test/resources</directory>
                <filtering>false</filtering>
                <excludes>
                    <exclude>**/*.properties</exclude>
                </excludes>
            </testResource>
        </testResources>
    </build>

Our default spring.profile.active value will be local. Do not forget to configure testResource for non filtered ones, if you do not to this, some of your test resources may be missing at runtime.

Follow the commit

Okay, so how I use this ?

If you process-test-resources it without any option

mvn clean process-test-resources

you will get the following properties in your target test-classes directory

application.properties

spring.profiles.active=local
spring.datasource.username=testhowto
spring.datasource.password=testhowto
spring.datasource.driver-class-name=org.postgresql.Driver

If you specify the spring.profiles.active variable with the ci value on command line

mvn clean package -Dspring.profiles.active=ci

you will get the following properties in your target test-classes directory

application.properties

spring.profiles.active=ci
spring.datasource.username=testhowto
spring.datasource.password=testhowto
spring.datasource.driver-class-name=org.postgresql.Driver

How to configure TestEntityManager

You might want to use TestEntityManager inside your test which add additional behavior to EntityManager to do some operations.

Add the @AutoConfigureTestEntityManager in combination with @Transactional to your test class.

If you use the TestEntityManager without an open transaction in a test class, Hibernate will complain about it, so @Transactional is mandatory here.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AdditionalTestConfig.class)
@AutoConfigureTestEntityManager
@Transactional
public class MyEntityServiceTest {

    @Autowired
    private TestEntityManager testEntityManager;

    @Test
    public void saveMyEntityShouldCreateNewEntity() {
        testEntityManager.persistAndGetId(new MyEntity());
    }
}

Follow the commit

How to inject sql dataset

You can use @Sql annotation above method for method specific dataset or above test class for a whole test class dataset

@Test
@Sql("/datasets/MyEntityServiceTest/injectedSqlTestMethod.sql")
public void injectedSqlTestMethod() {
    List<MyEntity> allMyEntity = myEntityService.getAllMyEntity();
    Assert.assertEquals(1, allMyEntity.size());
}

with the dataset defined in test resources root at /datasets/MyEntityServiceTest/injectedSqlTestMethod.sql

insert into my_entity values (1, 'my_value');

Follow the commit

How to test a REST Facade

If the spring-boot-starter-web is on the classpath, @SpringBootTest will automatically create a mock servlet environment for you. A WebApplicationContext instance will be automatically instantiated and available. Hence you can build up a MockMvc instance with MockMvcBuilders based on this application context.

The rest facade to test:

@RestController
@RequestMapping(MyRestFacade.ROOT_RESOURCE)
public class MyRestFacade {

    public static final String ROOT_RESOURCE = "/myrestfacade";

    @GetMapping
    public String getAString() {
        return "Hello !";
    }
}

The test

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyRestFacadeTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void before(){
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
    }

    @Test
    public void getAStringTest() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get(MyRestFacade.ROOT_RESOURCE))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

Follow the commit

How to test with Spring Security enabled

By default, Spring security will be ignored with the above configuration. For instance, if you add spring-boot-starter-security to your classpath, the above test would run perfectly fine even if all urls are now secured by default http BASIC security.

If you want to spring security configuration active, add the following dependency

   <dependency>
   		<groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <version>4.2.3.RELEASE</version>
        <scope>test</scope>
   </dependency>

And apply the SecurityMockMvcConfigurers.springSecurity to MockMvc

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyRestFacadeWithSpringSecurityTest {

    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mockMvc;

    @Before
    public void before() {
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .apply(SecurityMockMvcConfigurers.springSecurity())
                .build();
    }

    @Test
    public void getAStringTest() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get(MyRestFacade.ROOT_RESOURCE))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }
}

If you run the test, you should get:

java.lang.AssertionError: Status
Expected :200
Actual   :401

Follow the commit

How to mock Spring Security context

You can elegantly mock SecurityContext with a custom annotation. First, be sure to have spring-security-test in your classpath.

Here is a setup to Mock Http BASIC Authenticated User Security context (UsernamePasswordAuthenticationToken)

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockBasicAuthSecurityContextFactory.class)
public @interface WithMockBasicAuth {

    String principal();

    String[] roles() default {};
}

The factory which creates and sets the SecurityContext. Be sure to adapt the AuthenticationToken class according to the authentication scheme you choose for your app.

public class WithMockBasicAuthSecurityContextFactory implements WithSecurityContextFactory<WithMockBasicAuth>{

    @Override
    public SecurityContext createSecurityContext(WithMockBasicAuth basicAuth) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();

        for (final String role : basicAuth.roles()) {
            grantedAuthorities.add(new SimpleGrantedAuthority(role));
        }

        User principal = new User(basicAuth.principal(), "", grantedAuthorities);
        Authentication authentication = new UsernamePasswordAuthenticationToken(principal, null, grantedAuthorities);
        context.setAuthentication(authentication);
        return context;
    }
}

Now we can rewrite the test by using the annotation:

    @Test
    @WithMockBasicAuth(principal = "user", roles = "ROLE_USER")
    public void getAStringTest() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get(MyRestFacade.ROOT_RESOURCE))
                .andExpect(MockMvcResultMatchers.status().isOk());
    }

If you run it, you should now get a 200 and no more 401.

Follow the commit

How to mock a third party REST web service call

Let’s assume we have a service which retrieves data from a 3rd party REST data provider:

@Service
@Transactional
public class MyEntityService {    

    @Autowired
    private RestTemplate restTemplate;

 	public Map<String,String> getExternalData(){
        return restTemplate.getForObject("http://mythirdpartyhost/rest/api/data", Map.class);
    }

You can mock the 3rd party REST service by using a MockRestServiceServer instance. The MockServiceRestServer is build around the RestTemplate bean you declared in your testing configuration.

@TestConfiguration
public class AdditionalTestConfig {

    @Bean
    public AdditionnalTestComponent additionnalTestComponent() {
        return new AdditionnalTestComponent();
    }

    /**
     * A Mock if {@link MyComponent} class
     * @return the mock
     */
    @Bean
    public MyComponent component() {
        return Mockito.mock(MyComponent.class);
    }

    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

@RunWith(SpringRunner.class)
@SpringBootTest(classes = AdditionalTestConfig.class)
@AutoConfigureTestEntityManager
@Transactional
public class MyEntityServiceTest {    

    @Autowired
    private MyEntityService myEntityService;

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void getExternalData(){

        MockRestServiceServer mockRestServiceServer = MockRestServiceServer.createServer(restTemplate);

        mockRestServiceServer.expect(MockRestRequestMatchers.requestTo("http://mythirdpartyhost/rest/api/data"))
                .andRespond(MockRestResponseCreators.withSuccess("{ \"key\": \"value\" }", MediaType.APPLICATION_JSON));

        Map<String, String> externalData = myEntityService.getExternalData();

        Assert.assertEquals("value",externalData.get("key"));
    }
}

Follow the commit

How to mock SOAP web service call

In some cases you need to call some good old fashion SOAP Web service,

For dealing with SOAP web services I use the spring-boot-starter-web-services and the spring-ws-test extension.

dependencies

   <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web-services</artifactId>
   </dependency>
   <dependency>
      <groupId>org.springframework.ws</groupId>
      <artifactId>spring-ws-test</artifactId>
      <scope>test</scope>
   </dependency>

the method which does the call

    public void callASoapWebService(){

        WebServiceMessageCallback requestCallback = message -> {

            SoapMessage soapMessage = (SoapMessage) message;

            SoapHeader soapHeader = soapMessage.getSoapHeader();
            soapHeader.addHeaderElement(new QName("namespace", "username")).setText("user1");
            soapHeader.addHeaderElement(new QName("namespace", "password")).setText("password1");

        };

        WebServiceMessageCallback responseCallback = message -> {
        };

        webServiceTemplate.sendAndReceive("/CosignWS/CosignWS", requestCallback, responseCallback);
    }

relying on the following spring configuration

@Configuration
public class MyConfig {

    @Bean
    public WebServiceTemplate getWebServiceTemplate() {
        return new WebServiceTemplate();
    }
}

and the corresponding test which use MockWebServiceServer class to mock SOAP response and SOAP request

    @Test
    public void callASoapWebServiceTest() throws URISyntaxException, IOException {

        URL resourceRequestXml = getClass().getResource("/soap/soap-request.xml");
        byte[] resourceRequestBytes = Files.readAllBytes(Paths.get(resourceRequestXml.toURI()));
        String xmlSourceRequest = new String(resourceRequestBytes, StandardCharsets.UTF_8);

        URL resourceResponseXml = getClass().getResource("/soap/soap-response.xml");
        byte[] resourceResponseBytes = Files.readAllBytes(Paths.get(resourceResponseXml.toURI()));
        String xmlSourceResponse = new String(resourceResponseBytes, StandardCharsets.UTF_8);

        Source requestPayload = new StringSource(xmlSourceRequest);

        Source responsePayload = new StringSource(xmlSourceResponse);

        MockWebServiceServer server = MockWebServiceServer.createServer(webServiceTemplate);

        server.expect(RequestMatchers.soapEnvelope(requestPayload))
                .andRespond(ResponseCreators.withSoapEnvelope(responsePayload));

        myEntityService.callASoapWebService();

        server.verify();
    }

Note that the above test uses two classpath resources to make test more readable and maintainable:

SOAP request

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP-ENV:Header>
        <username xmlns="namespace">user1</username>
        <password xmlns="namespace">password1</password>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

SOAP response

<?xml version='1.0' encoding='UTF-8'?>
<S:Envelope
	xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
	<S:Body>
	</S:Body>
</S:Envelope>

Follow the commit

How to write unit test

Funny How-to title isn’it ? In most cases Unit testing is okay to ensure that your app does what it has to do. Besides, unit tests run fast, and they can sometimes allow you to cover more uncommon cases than database integrated test. How to ensure that your method is really null-safe if your test relies on an injected database dataset which cannot inject null fields because you schema does not allow it ?

By using Spring Framework the right thing to do here is to get rid of field injection and use constructor injection instead. I used to use field injection for years, it was widely debated on the web but I must confess that relying on immutable component and using field injection is a pretty unconformable theoretical mix.


@Service
public class MyOtherService {

    @Autowired
    private MyComponent myComponent;

    public String doSomething(){
        return myComponent.giveMeAValue();
    }
}

could be rewritten to


@Service
public class MyOtherService {

    private final MyComponent myComponent;

    public MyOtherService(MyComponent myComponent) {
        this.myComponent = myComponent;
    }

    public String doSomething(){
        return myComponent.giveMeAValue();
    }
}

and unit testing becomes a straightforward 2 steps process: create the component with mock dependencies and testing it (So no more @InjectMock things, and no more spring context at all).

@RunWith(JUnit4.class)
public class MyOtherServiceUnitTest {

    @Test
    public void aTest() {

        MyComponent myComponent = Mockito.mock(MyComponent.class);
        Mockito.when(myComponent.giveMeAValue()).thenReturn("mock value");
        MyOtherService myOtherService = new MyOtherService(myComponent);

        String result = myOtherService.doSomething();

        Assert.assertEquals("mock value", result);
    }
}

…But but but I hate writing boilerplate constructors !!

okay, okay, in real life projects I extensively use Project Lombok

The tip here is to use the @RequiredArgsConstructor annotation:

@Service
@RequiredArgsConstructor
public class MyOtherService {

    private final MyComponent myComponent;  

    public String doSomething(){
        return myComponent.giveMeAValue();
    }
}

Follow the commit

Thanks for reading

Testing is an everyday topic for all developers and I really enjoyed writing this article. Hope it may be useful to you !

sources : https://github.com/slem1/spring-boot-test-howto

Comments