Master Spring Boot Testing: Unit and Integration Tests with JUnit, Hamcrest, and MockMvc
This tutorial walks through writing and running unit and integration tests for a Spring Boot application, covering JUnit and Hamcrest assertions, Maven setup, model definitions, service and controller implementations, MockMvc usage, and both mocked and real‑world test scenarios, complete with sample code and output.
1. Overview
In this article we learn how to write unit tests and integrate them in a Spring Boot environment. While many tutorials exist online, they often scatter the necessary information across multiple pages. This guide clarifies the use of different annotations in various contexts.
2. Unit Test vs. Integration Test
In computer programming, a unit test is a software testing method that tests a single unit of source code, a single or multiple program modules, and related control data, usage processes, and operations to determine whether they are fit for use.
Integration test (sometimes called I&T) is a stage of software testing where individual software modules are combined and tested together.
In short, unit tests test a single code unit (typically one method) without involving other components, whereas integration tests verify the interaction between components.
3. Java Unit Test
All Java developers know JUnit is the primary framework for executing unit tests. It provides many annotations for assertions.
Hamcrest is an additional framework for software testing. Hamcrest allows using existing matcher classes to check conditions and also to create custom matchers. To use Hamcrest matchers in JUnit you must use the assertThat statement followed by one or more matchers.
Here is a simple test 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);
}
@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")));
}
@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. Example Case
Let's write a simple program that provides a basic search engine for manga.
4.1 Maven Dependencies
First add the following dependencies to the 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 Model
4.2.1 Manga class
The Manga class represents a manga instance returned by the system. Lombok is used 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 that contains a list of Manga objects.
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 Implement Service
The service uses the free Jikan Moe API. RestTemplate is used to make REST calls.
package com.mgiglione.service;
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 Implement Controller
The controller exposes two endpoints, one synchronous and one asynchronous, for testing purposes.
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("/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("title") String title) {
return CompletableFuture.completedFuture(mangaService.getMangasByTitle(title));
}
@RequestMapping(value = "/sync/{title}", method = RequestMethod.GET)
public @ResponseBody List<Manga> searchSync(@PathVariable("title") String title) {
return mangaService.getMangasByTitle(title);
}
}4.5 Run and Test the System
mvn spring-boot:runThen try:
curl http://localhost:8080/manga/async/ken
curl http://localhost:8080/manga/sync/kenSample JSON output (truncated) shows title, description, volumes, and score for several manga entries.
5. Spring Boot Application Unit Tests
Spring Boot provides the @SpringBootTest annotation to simplify testing.
5.1 Unit Test for MangaService
We mock RestTemplate to always return a fixed response.
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
private RestTemplate template;
@Test
public void testGetMangasByTitle() throws IOException {
MangaResult mRs = JsonUtils.jsonFile2Object("ken.json", MangaResult.class);
when(template.getForEntity(any(String.class), any(Class.class)))
.thenReturn(new ResponseEntity(mRs, HttpStatus.OK));
List<Manga> mangasByTitle = mangaService.getMangasByTitle("goku");
assertThat(mangasByTitle).isNotNull()
.isNotEmpty()
.allMatch(p -> p.getTitle().toLowerCase().contains("ken"));
}
}5.2 Unit Test for MangaController
We mock MangaService and use MockMvc in a standalone setup.
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 com.mgiglione.controller.MangaController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerUnitTest {
MockMvc mockMvc;
@Autowired
private MangaController mangaController;
@MockBean
MangaService mangaService;
private List<Manga> mangas;
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(this.mangaController).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...")
.build();
mangas = new ArrayList<>();
mangas.add(manga1);
mangas.add(manga2);
}
@Test
public void testSearchSync() throws Exception {
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 {
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())
.andReturn();
mockMvc.perform(asyncDispatch(result))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].title", is("Hokuto no ken")));
}
}6. Integration Tests
6.1 Integration Test for MangaService
No mocking is required; the test calls the real remote Manga 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 Test for MangaController
We use MockMvc with the real service in a standalone setup.
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 com.mgiglione.controller.MangaController;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerIntegrationTest {
MockMvc mockMvc;
@Autowired
private MangaController mangaController;
@Before
public void setup() throws Exception {
this.mockMvc = standaloneSetup(this.mangaController).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())
.andReturn();
mockMvc.perform(asyncDispatch(result))
.andDo(print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
}
}7. Conclusion
We have learned the main differences between unit testing and integration testing in a Spring Boot environment, and seen how frameworks like Hamcrest simplify test writing. All source code is available in the author's GitHub repository.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
