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.

Programmer DD
Programmer DD
Programmer DD
Master Spring Boot Testing: Unit and Integration Tests with JUnit, Hamcrest, and MockMvc

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:run

Then try:

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

Sample 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

testingSpring BootJUnitIntegration TestUnit TestMockMvcHamcrest
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.