Testing in Spring Boot: A Comprehensive Guide

In today’s fast-paced development environment, ensuring code reliability is crucial. Spring Boot, a popular Java-based framework for creating stand-alone, production-grade applications, makes testing a first-class citizen in its ecosystem. Whether building REST APIs, data access layers, or service components, robust testing helps developers catch issues early, enhance maintainability, and build user trust.

This guide explores how to conduct adequate testing in Spring Boot, covering unit testing, integration testing, best practices, and tooling.

1. Why Testing Matters in Spring Boot

Spring Boot emphasizes convention over configuration, which accelerates development. However, rapid development without proper testing can lead to unstable software. Testing in Spring Boot ensures:

  • Components behave as expected
  • Dependencies are correctly wired
  • External integrations work properly
  • Refactoring doesn’t break existing functionality

Spring Boot provides various testing utilities out of the box, such as annotations (@SpringBootTest@MockitoBean@WebMvcTest, etc.) and support for tools like JUnit 5, Mockito, and Testcontainers.

2. Types of Tests in Spring Boot

Prepare for the Class testing.

Create User

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.RequiredArgsConstructor;


@Entity
@Getter
@Setter
@Table(name = "\"user\"") // Quote the table name
@RequiredArgsConstructor
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

}

Create Service

import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(Long id) {
        return userRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
    }

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    public User createUser(User user) {
        return userRepository.save(user);
    }

    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

Create Respository

import com.example.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {}

Unit Testing

Unit tests focus on testing individual components in isolation. These are fast, easy to maintain, and great for catching issues early.

Example: Testing a Service Class

import com.example.controller.UserController;
import com.example.entity.User;
import com.example.service.UserService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserControllerTest {

    @Mock
    private UserService userService;

    @InjectMocks
    private UserController userController;

    @Test
    void getUser_existingUser_returnsOk() {
        User user = new User(1L, "John Doe");
        when(userService.getUserById(1L)).thenReturn(user);

        ResponseEntity<User> response = userController.getUser(1L);

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(user, response.getBody());
    }

    @Test
    void getUser_nonExistingUser_returnsNotFound() {
        when(userService.getUserById(1L)).thenReturn(null);

        ResponseEntity<User> response = userController.getUser(1L);

        assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
    }
}
Test result

Integration Testing

Integration tests verify how multiple components work together. These tests often interact with accurate databases or simulate APIs. It required a Docker Container on your machine.

Add Maven dependency

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.10.1</version>
    <scope>test</scope>
</dependency>

Create a SQL script to initialize the Table User.

-- src/test/resources/init.sql
CREATE TABLE "user" (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255)
);

INSERT INTO "user" (name) VALUES ('Alice');
INSERT INTO "user" (name) VALUES ('Bob');

Directory

src/test/resources/init.sql

Using @SpringBootTest with Testcontainers

package com.example.jobrunr.qc;

import com.example.jobrunr.entity.User;
import com.example.jobrunr.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class UserIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("testuser")
            .withPassword("testpassword").withInitScript("init.sql");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private UserRepository userRepository;

    @Test
    void getUser_existingUser_returnsOk() {
        User user = new User(null, "Jane Smith");
        user = userRepository.save(user);

        ResponseEntity<User> response = restTemplate.getForEntity("/users/" + user.getId(), User.class);

        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertEquals(user.getName(), response.getBody().getName());
    }
}
Test result

3. Web Layer Testing

Testing the web layer allows developers to validate HTTP requests and responses without loading the full application context.

MockMvc with @WebMvcTest

package com.example.jobrunr.qc;

import com.example.jobrunr.controller.UserController;
import com.example.jobrunr.entity.User;
import com.example.jobrunr.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
class UserControllerWebMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean // Using the updated MockBean
    private UserService userService;

    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void testGetUser() throws Exception {
        User user = new User(1L, "Charlie");
        Mockito.when(userService.getUserById(1L)).thenReturn(user);

        mockMvc.perform(get("/users/1")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("Charlie"));
    }
}

@WebMvcTest is perfect for isolating your MVC layer and running fast tests without hitting the database.

4. Data Layer Testing

Spring Boot simplifies testing JPA repositories with @DataJpaTest. This loads only repository beans and configures an in-memory database by default (like H2).

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void testFindByEmail() {
        User user = new User(null, "Dana", "dana@example.com");
        userRepository.save(user);

        User result = userRepository.findByEmail("dana@example.com");
        assertNotNull(result);
        assertEquals("Dana", result.getName());
    }
}

5. Test Configuration and Profiles

Developers can configure separate profiles for testing using application-test.yml or @ActiveProfiles("test"). This lets you:

  • Use different databases (e.g., H2 instead of PostgreSQL)
  • Mock external services
  • Override beans for test behavior
# application-test.yml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:

In your test class:

@ActiveProfiles("test")
@SpringBootTest
class UserServiceTest { ... }

6. Mocking and Dependency Injection

Spring Boot integrates seamlessly with Mockito, enabling developers to mock dependencies instead of real objects.

Tips:

  • Use @MockitoBean to mock Spring-managed beans
  • Use @Mock for raw unit tests (non-Spring tests)

7. Test Coverage and Reporting

Tools like JaCoCo and SonarQube help visualize test coverage and identify untested paths. Adding JaCoCo to your pom.xml is simple:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Run mvn jacoco:reportand locate your target/site/jacoco/index.html report.

jacoco report
index.html

8. Best Practices for Testing in Spring Boot

Best Practices

Conclusion

Spring Boot offers powerful testing features that are right out of the box. From simple unit tests to complex integration tests with actual containers, the framework equips developers with all the tools to build reliable, maintainable applications.

By embracing testing as part of your development lifecycle, developers ship better software and gain confidence with every change. Start small, automate regularly, and grow your suite into a safety net that helps developers deliver with speed and quality.

Leave a Comment

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