1. Test in Spring Boot Overview

In this post, we’ll have a look at how to write unit and integration tests in a Spring boot environment. You can find tons of tutorials online on this topic but it is very difficult to find all the information that you need in just one page. I often noticed that junior developers are confusing between unit and integration test expecially when speaking about spring ecosystem and I’ll try to clarify the usage of different annotations used in different contexts.

2. Unit vs Integration tests

Wikipedia says about unit testing : “In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.”

and about Integration testing: “Integration testing (sometimes called integration and testing, abbreviated I&T) is the phase in software testing in which individual software modules are combined and tested as a group.”

In simple words when we do unit test we test just a single unit of code, one method at a time, excluding all other components that interact with our under testing one.

In integration tests on the other side, we test the integration between components. Thanks to unit testing, we know that components behave as required individually, but we don’t know how they’ll work all together. This is the responsibility of integration tests.

3. Java Test Unit

About all java developers knows about JUnit as the main framework to perform test unit. It offers a lot of annotations to make assertions on expectations.
Hamcrest is an additional framework for software tests. Hamcrest allows checking for conditions in your code using existing matchers classes and it also allows you to define your custom matcher implementations. To use Hamcrest matchers in JUnit you have to use the assertThat statement followed by one or several matchers.
Here you can see simple tests using both frameworks.

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;

import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;

public class AssertTests {
  @Test
  public void testAssertArrayEquals() {
    byte[] expected = "trial".getBytes();
    byte[] actual = "trial".getBytes();
    assertArrayEquals("failure - byte arrays not same", expected, actual);
  }

  @Test
  public void testAssertEquals() {
    assertEquals("failure - strings are not equal", "text", "text");
  }

  @Test
  public void testAssertFalse() {
    assertFalse("failure - should be false", false);
  }

  @Test
  public void testAssertNotNull() {
    assertNotNull("should not be null", new Object());
  }

  @Test
  public void testAssertNotSame() {
    assertNotSame("should not be same Object", new Object(), new Object());
  }

  @Test
  public void testAssertNull() {
    assertNull("should be null", null);
  }

  @Test
  public void testAssertSame() {
    Integer aNumber = Integer.valueOf(768);
    assertSame("should be same", aNumber, aNumber);
  }

  // JUnit Matchers assertThat
  @Test
  public void testAssertThatBothContainsString() {
    assertThat("albumen", both(containsString("a")).and(containsString("b")));
  }

  @Test
  public void testAssertThatHasItems() {
    assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
  }

  @Test
  public void testAssertThatEveryItemContainsString() {
    assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
  }

  // Core Hamcrest Matchers with assertThat
  @Test
  public void testAssertThatHamcrestCoreMatchers() {
    assertThat("good", allOf(equalTo("good"), startsWith("good")));
    assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
    assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
    assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
    assertThat(new Object(), not(sameInstance(new Object())));
  }

  @Test
  public void testAssertTrue() {
    assertTrue("failure - should be true", true);
  }
}

4. Introducing our example

Let’s write our simple application. The idea is to provide a basic search engine for mangas.

An example on how to test in spring boot application
kenhiro vs raul

 

4.1. Maven dependencies

First of all we need to add some dependency to our project

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.16.20</version>
  <scope>provided</scope>
</dependency>

4.2. Define the Model

Our model is really simple; it is made up by only two class: Manga and MangaResult

4.2.1. Manga Class

Manga class represents an instance of manga as retrieved by the system. I used lombok project to reduce boilerplate code.

package com.mgiglione.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Manga {
    private String title;
    private String description;
    private Integer volumes;
    private Double score;
}
4.2.2. MangaResult

MangaResult is a wrapper class that contains a List of Mangas.

package com.mgiglione.model;

import java.util.List;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter @Setter @NoArgsConstructor
public class MangaResult {
    private List<Manga> result;
}

4.3. Implementing the Service

For implementing the service, we will use API freely exposed by Jikan Moe.

RestTemplate is the spring class that I use to make rest calls to the API.

package com.mgiglione.service;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;

@Service
public class MangaService {

    Logger logger = LoggerFactory.getLogger(MangaService.class);
    private static final String MANGA_SEARCH_URL="http://api.jikan.moe/search/manga/";

    @Autowired
    RestTemplate restTemplate;
      
    public List<Manga> getMangasByTitle(String title) {
        return restTemplate.getForEntity(MANGA_SEARCH_URL+title, MangaResult.class).getBody().getResult();
    }

}

4.4. Implementing the Controller

Next step on the list is to write down the rest controller that exposes two endpoints, one synchronous and one asynchronous just for testing purpose. This controller makes use of the Service defined above.

package com.mgiglione.controller;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;

@RestController
@RequestMapping(value = "/manga")
public class MangaController {

    Logger logger = LoggerFactory.getLogger(MangaController.class);

    @Autowired
    private MangaService mangaService;   

    @RequestMapping(value = "/async/{title}", method = RequestMethod.GET)
    @Async
    public CompletableFuture<List<Manga>> searchASync(@PathVariable(name = "title") String title) {
        return CompletableFuture.completedFuture(mangaService.getMangasByTitle(title));
    }

    @RequestMapping(value = "/sync/{title}", method = RequestMethod.GET)
    public @ResponseBody <List<Manga>> searchSync(@PathVariable(name = "title") String title) {
        return mangaService.getMangasByTitle(title);
    }

}

4.5. Launching and Testing the System

Start the spring boot application using maven:

mvn spring-boot:run

Then let’s try it:

curl http://localhost:8080/manga/async/ken
curl http://localhost:8080/manga/sync/ken

Example of output

{
"title": "Rurouni Kenshin: Meiji Kenkaku Romantan",
"description": "Ten years have passed since the end of Bakumatsu, an era of war that saw the uprising of citizens against the Tokugawa shogunate. The revolutionaries wanted to create a time of peace, and a thriving c...",
"volumes": 28,
"score": 8.69
},
{
"title": "Sun-Ken Rock",
"description": "The story revolves around Ken, a man from an upper-class family that was orphaned young due to his family's involvement with the Yakuza; he became a high school delinquent known for fighting. The only...",
"volumes": 25,
"score": 8.12
},
{
"title": "Yumekui Kenbun",
"description": "For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....",
"volumes": 9,
"score": 7.97
}

5. Unit Testing Spring Boot Application

Spring boot offers a great class to make testing easier: @SpringBootTest annotation

This annotation can be specified on a test class that runs Spring Boot based tests.
Provides the following features over and above the regular Spring TestContext Framework:

    • Uses SpringBootContextLoader as the default ContextLoader when no specific @ContextConfiguration(loader=…) is defined.
    • Automatically searches for a @SpringBootConfiguration when nested @Configuration is not used, and no explicit classes are specified.
    • Allows custom Environment properties to be defined using the properties attribute.
    • Provides support for different webEnvironment modes, including the ability to start a fully running web server listening on a defined or random port.
    • Registers a TestRestTemplate and/or WebTestClient bean for use in web tests that are using a fully running web server.

 

We basically have two components to test here: MangaService and MangaController

5.1. Unit testing MangaService

To test MangaService we need to isolate it from external components. In our case we only have one external component required: RestTemplate that we use to call remote API.
What we need to do is to mock the RestTemplate bean and let it always respond with a fixed given response.
Spring test incorporates and extends Mockito library to configure mocked beans through @MockBean annotation.

package com.mgiglione.service.test.unit;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import java.io.IOException;
import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;

import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;
import com.mgiglione.service.MangaService;
import com.mgiglione.utils.JsonUtils;

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

    @Autowired
    private MangaService mangaService;

    // MockBean is the annotation provided by Spring that wraps mockito one
    // Annotation that can be used to add mocks to a Spring ApplicationContext.
    // If any existing single bean of the same type defined in the context will be replaced by the mock, if no existing bean is defined a new one will be added.
    @MockBean
    private RestTemplate template;

    @Test
    public void testGetMangasByTitle() throws IOException {
        // Parsing mock file
        MangaResult mRs = JsonUtils.jsonFile2Object("ken.json", MangaResult.class);
        // Mocking remote service
        when(template.getForEntity(any(String.class), any(Class.class))).thenReturn(new ResponseEntity(mRs, HttpStatus.OK));
        // I search for goku but system will use mocked response containing only ken, so I can check that mock is used.
        List<Manga> mangasByTitle = mangaService.getMangasByTitle("goku");
        assertThat(mangasByTitle).isNotNull()
            .isNotEmpty()
            .allMatch(p -> p.getTitle()
                .toLowerCase()
                .contains("ken"));

    }

}

5.2. Unit testing MangaController

As done in the unit testing of the service we need to isolate components. In this case, we need to mock the MangaService bean.
Then we have a little further problem… Controller part is the part of the system that manages HttpRequest so we need a system to simulate this behavior without starting a full HTTP server.
MockMvc is the Spring class that does that. It can be setted up in different ways:

      1. Using Standalone Context
      2. Using WebApplication Context
      3. Let’s spring autoconfigure it by loading all context by using these annotations on test class @SpringBootTest @AutoConfigureMockMvc
      4. Let’s spring autoconfigure it by loading just web layer context by using these annotations on test class @WebMvcTest
package com.mgiglione.service.test.unit;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

import java.util.ArrayList;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;

import com.mgiglione.controller.MangaController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;

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

    MockMvc mockMvc;

    @Autowired
    protected WebApplicationContext wac;

    @Autowired
    MangaController mangaController;

    @MockBean
    MangaService mangaService;
    
    /**
     * List of samples mangas
     */
    private List<Manga> mangas;

    @Before
    public void setup() throws Exception {
        this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
        // mockMvc = MockMvcBuilders.webAppContextSetup(wac)
        // .build();
        Manga manga1 = Manga.builder()
            .title("Hokuto no ken")
            .description("The year is 199X. The Earth has been devastated by nuclear war...")
            .build();
        Manga manga2 = Manga.builder()
            .title("Yumekui Kenbun")
            .description("For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....")
            .build();

        mangas = new ArrayList<>();
        mangas.add(manga1);
        mangas.add(manga2);
    }

    @Test
    public void testSearchSync() throws Exception {
        
        // Mocking service
        when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);

        mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title", is("Hokuto no ken")))
            .andExpect(jsonPath("$[1].title", is("Yumekui Kenbun")));
    }

    @Test
    public void testSearchASync() throws Exception {
       

        // Mocking service
        when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);

        MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(request().asyncStarted())
            .andDo(print())
            // .andExpect(status().is2xxSuccessful()).andReturn();
            .andReturn();

        // result.getRequest().getAsyncContext().setTimeout(10000);

        mockMvc.perform(asyncDispatch(result))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title", is("Hokuto no ken")));

    }
}

As you can see from the code I chose the first solution because is the lightest one, and we have the best governance on what we load in the Spring context.
In the async test I had to simulate the asynchronous behavior by first calling service and then starting the asyncDispatch method.

6. Integration Testing Spring Boot Application

For the integration tests we want to check our main components with downstream communication.

6.1. Integration Testing of MangaService

This test is really simple. We don’t need to Mock anything because we want to call remote mangas API.

package com.mgiglione.service.test.integration;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MangaServiceIntegrationTest {
    
    @Autowired
    private MangaService mangaService;
    
    @Test
    public void testGetMangasByTitle() {
           List<Manga> mangasByTitle = mangaService.getMangasByTitle("ken");
           assertThat(mangasByTitle).isNotNull().isNotEmpty();
    }

}

6.2. Integration Testing of MangaController

This test is pretty similar to Unit test one but in this case we haven’t a mocked service.

package com.mgiglione.service.test.integration;

import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;

import com.mgiglione.controller.MangaController;

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

    // @Autowired
    MockMvc mockMvc;
    

    @Autowired
    protected WebApplicationContext wac;

    @Autowired
    MangaController mangaController;

    @Before
    public void setup() throws Exception {
        this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
        // mockMvc = MockMvcBuilders.webAppContextSetup(wac)
        // .build();
    }

    @Test
    public void testSearchSync() throws Exception {
        mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
    }

    @Test
    public void testSearchASync() throws Exception {
        MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(request().asyncStarted())
            .andDo(print())
            .andReturn();

        mockMvc.perform(asyncDispatch(result))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));

    }



}

7. Conclusions

We have seen main differences between Unit and Integration tests in a Spring Boot Environment, giving a look also at frameworks like hamcrest that simplify test writing.
We tested both service and controller, with and without asynchronous behavior. If you want you can take a look at my article on webflux that shows how to test reactive controllers.

Of course, you can find anything in my git hub repository.

Leave a Reply

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