Unit testing is a critical part of building reliable and maintainable Spring Boot applications. By writing unit tests, developers can verify that individual components—such as services, controllers, and utility classes—work as expected in isolation.
In the Spring Boot ecosystem, unit testing is commonly done using JUnit 5 as the testing framework and Mockito as the mocking library. Together, they allow you to test application logic without relying on real databases, external services, or full application context startup.
In this tutorial, you’ll learn how to write clean, effective unit tests in Spring Boot using JUnit and Mockito. We’ll cover everything from basic test setup to testing service and controller layers, along with best practices used in real-world projects.
By the end of this guide, you’ll be able to:
-
Write unit tests for Spring Boot services and controllers
-
Mock dependencies using Mockito
-
Understand when to use
@WebMvcTestvs@SpringBootTest -
Structure your tests for readability and maintainability
Let’s start by setting up a Spring Boot project with the necessary testing dependencies.
Project Setup
Before writing any unit tests, we need a Spring Boot project that is properly configured for testing with JUnit 5 and Mockito. Spring Boot makes this setup straightforward by providing built-in testing support.
1. Creating a Spring Boot Project
You can create a new Spring Boot project using Spring Initializr or your preferred IDE (IntelliJ IDEA, Eclipse, VS Code).
Recommended configuration:
-
Project: Maven
-
Language: Java
-
Spring Boot: 3.x (latest stable)
-
Java: 17 or newer
-
Packaging: Jar
-
Dependencies:
-
Spring Web
-
Spring Data JPA (optional, if you want repository examples)
-
H2 or PostgreSQL (optional)
-
This tutorial focuses on unit testing, so a simple REST API structure is more than enough.
2. Understanding spring-boot-starter-test
Spring Boot provides a dedicated testing starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
This single dependency includes everything we need for unit testing:
-
JUnit Jupiter (JUnit 5) – test framework
-
Mockito – mocking dependencies
-
AssertJ – fluent assertions
-
Hamcrest – matcher-based assertions
-
Spring Test – Spring-specific testing utilities
-
JSONassert & JsonPath – JSON testing support
👉 By default, Spring Boot excludes JUnit 4, so you don’t need to remove anything manually.
3. Verifying the Maven pom.xml
Your pom.xml should contain at least the following testing dependency:
<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>
If you want to explicitly use the latest Mockito version, you can add:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
⚠️ This is optional—Spring Boot already manages Mockito versions for you.
4. Project Structure for Testing
Spring Boot follows a clear convention for test placement.
src
├── main
│ └── java
│ └── com.example.demo
│ ├── controller
│ ├── service
│ ├── repository
│ └── DemoApplication.java
└── test
└── java
└── com.example.demo
├── controller
├── service
└── DemoApplicationTests.java
Key points:
-
Tests live under
src/test/java -
The package structure mirrors
src/main/java -
Each layer (controller, service) has its own test class
5. Creating a Sample Service to Test
Let’s add a simple service that we’ll use in later sections.
@Service
public class CalculatorService {
public int add(int a, int b) {
return a + b;
}
public int subtract(int a, int b) {
return a - b;
}
}
This small service is perfect for demonstrating unit testing fundamentals without distractions.
6. Running Tests in Spring Boot
Spring Boot automatically detects and runs tests that:
-
Are annotated with
@Test -
Follow naming conventions like
*Testor*Tests
You can run tests using:
-
IDE: Right-click → Run Test
-
Command line:
mvn test
If everything is configured correctly, Maven should report BUILD SUCCESS.
Understanding JUnit 5 Basics
JUnit 5 is the modern testing framework used by Spring Boot for writing and running unit tests. In this section, we’ll cover the essential JUnit 5 concepts you need to write clean, readable, and effective unit tests.
1. What Is JUnit 5?
JUnit 5 is a complete rewrite of JUnit with a modular architecture. It consists of three main parts:
-
JUnit Platform – The foundation that launches testing frameworks on the JVM
-
JUnit Jupiter – The API for writing tests and extensions (what we use most)
-
JUnit Vintage – Support for running legacy JUnit 4 tests (optional)
Spring Boot uses JUnit Jupiter by default, so you can start writing JUnit 5 tests immediately.
2. Creating Your First JUnit 5 Test
Let’s write a simple unit test for the CalculatorService we created earlier.
Create a new test class under src/test/java:
class CalculatorServiceTest {
private CalculatorService calculatorService;
@BeforeEach
void setUp() {
calculatorService = new CalculatorService();
}
@Test
void shouldAddTwoNumbers() {
int result = calculatorService.add(5, 3);
assertEquals(8, result);
}
}
✔️ This is a pure unit test:
-
No Spring context
-
No database
-
No mocks (yet)
3. Core JUnit 5 Annotations
JUnit 5 provides several useful annotations:
| Annotation | Description |
|---|---|
@Test |
Marks a method as a test case |
@BeforeEach |
Runs before each test method |
@AfterEach |
Runs after each test method |
@BeforeAll |
Runs once before all tests (must be static) |
@AfterAll |
Runs once after all tests (must be static) |
@Disabled |
Skips a test |
Example:
@BeforeAll
static void initAll() {
System.out.println("Runs once before all tests");
}
@AfterEach
void tearDown() {
System.out.println("Runs after each test");
}
4. Assertions in JUnit 5
Assertions are used to verify expected results.
Common assertions:
assertEquals(expected, actual);
assertNotNull(object);
assertTrue(condition);
assertFalse(condition);
assertThrows(Exception.class, executable);
Example with exception testing:
@Test
void shouldThrowExceptionWhenDividingByZero() {
assertThrows(ArithmeticException.class, () -> {
int result = 10 / 0;
});
}
JUnit 5 also supports lambda-based assertions, which improve readability.
5. Display Names for Readable Tests
You can use @DisplayName to make the test output more descriptive.
@Test
@DisplayName("Add two numbers and return the correct result")
void addTwoNumbers() {
int result = calculatorService.add(2, 3);
assertEquals(5, result);
}
This is especially useful in larger projects and CI pipelines.
6. Grouping Tests with Nested Classes
JUnit 5 allows grouping related tests using @Nested.
@Nested
class AdditionTests {
@Test
void shouldAddPositiveNumbers() {
assertEquals(10, calculatorService.add(5, 5));
}
@Test
void shouldAddNegativeNumbers() {
assertEquals(-4, calculatorService.add(-2, -2));
}
}
This helps organize tests logically and improves readability.
7. Naming Conventions and Best Practices
Recommended test naming patterns:
-
shouldDoSomethingWhenCondition -
methodName_expectedResult -
givenCondition_whenAction_thenResult
Example:
shouldReturnSumWhenAddingTwoNumbers()
Follow the Arrange – Act – Assert (AAA) pattern:
-
Arrange test data
-
Act on the method under test
-
Assert the result
8. When NOT to Use Spring in Unit Tests
For true unit tests:
-
❌ Do NOT use
@SpringBootTest -
❌ Do NOT load the application context
-
❌ Do NOT inject real beans
JUnit + plain Java objects = fast and reliable tests
We’ll introduce Spring-specific testing only when needed.
Introduction to Mockito
In real-world Spring Boot applications, classes rarely work in isolation. Services often depend on repositories, external APIs, or other services. Mockito allows us to replace those dependencies with mock objects, so we can test business logic without relying on real implementations.
1. What Is Mockito?
Mockito is a popular Java mocking framework used to:
-
Create mock objects
-
Define how mocks behave
-
Verify interactions with dependencies
With Mockito, you can focus on what your class does, not what its dependencies do.
2. Why Mockito Is Essential for Unit Testing
Without mocking, unit tests can:
-
Be slow
-
Depend on databases or network calls
-
Break when external services change
Mockito solves this by allowing you to:
-
Simulate repository results
-
Force exceptions
-
Verify method calls
👉 Rule of thumb:
If a class depends on something outside itself, mock it.
3. Adding Mockito (Already Included)
Spring Boot includes Mockito via:
spring-boot-starter-test
So no extra setup is required for most projects.
4. Creating a Service with a Dependency
Let’s create a simple service that depends on a repository.
@Repository
public interface UserRepository {
Optional<User> findById(Long id);
}
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
}
5. Writing a Unit Test with Mockito
Now let’s write a unit test using Mockito.
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void shouldReturnUserWhenUserExists() {
User user = new User(1L, "John");
when(userRepository.findById(1L))
.thenReturn(Optional.of(user));
User result = userService.getUserById(1L);
assertEquals("John", result.getName());
}
}
What’s happening here?
-
@ExtendWith(MockitoExtension.class)
Enables Mockito support in JUnit 5 -
@Mock
Creates a mock ofUserRepository -
@InjectMocks
Injects mocks intoUserService -
when(...).thenReturn(...)
Defines mock behavior
6. Verifying Interactions
Mockito can verify that methods are called as expected.
@Test
void shouldCallRepositoryOnce() {
when(userRepository.findById(1L))
.thenReturn(Optional.of(new User(1L, "John")));
userService.getUserById(1L);
verify(userRepository, times(1)).findById(1L);
}
Useful verification methods:
-
times(n) -
never() -
atLeastOnce()
7. Testing Exceptions with Mockito
You can also test exception scenarios.
@Test
void shouldThrowExceptionWhenUserNotFound() {
when(userRepository.findById(1L))
.thenReturn(Optional.empty());
assertThrows(RuntimeException.class, () -> {
userService.getUserById(1L);
});
}
8. Stubbing vs Mocking (Quick Concept)
-
Mocking: Creating fake objects
-
Stubbing: Defining return values or behavior
Example:
when(repository.save(any()))
.thenThrow(new RuntimeException("DB error"));
9. Common Mockito Pitfalls
❌ Forgetting @ExtendWith(MockitoExtension.class)
❌ Mixing Mockito with @SpringBootTest unnecessarily
❌ Mocking the class under test
❌ Overusing verify() instead of asserting behavior
Unit Testing the Service Layer with Mockito
The service layer contains your core business logic. This makes it the most important layer to test thoroughly. In this section, you’ll learn how to write clean, fast, and isolated unit tests for Spring Boot services using JUnit 5 and Mockito.
1. What Should Be Tested in the Service Layer?
Service unit tests should focus on:
-
Business rules and calculations
-
Conditional logic (if/else, validations)
-
Interactions with repositories
-
Exception handling
They should not:
-
Start the Spring context
-
Access a real database
-
Use
@SpringBootTest
2. Example Domain Model
Let’s extend our example with a simple User entity.
public class User {
private Long id;
private String name;
private String email;
// constructors, getters, setters
}
3. Repository Interface (Dependency)
@Repository
public interface UserRepository {
Optional<User> findById(Long id);
User save(User user);
boolean existsByEmail(String email);
}
4. Service Implementation
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(User user) {
if (userRepository.existsByEmail(user.getEmail())) {
throw new IllegalArgumentException("Email already exists");
}
return userRepository.save(user);
}
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
}
}
5. Setting Up the Service Test Class
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
}
✔️ No Spring context
✔️ Fast execution
✔️ Fully isolated tests
6. Testing a Successful Service Method
@Test
void shouldCreateUserWhenEmailDoesNotExist() {
User user = new User(null, "John", "[email protected]");
when(userRepository.existsByEmail("[email protected]"))
.thenReturn(false);
when(userRepository.save(user))
.thenReturn(user);
User result = userService.createUser(user);
assertNotNull(result);
assertEquals("John", result.getName());
verify(userRepository).save(user);
}
Key points:
-
Mock dependencies
-
Assert returned values
-
Verify interactions
7. Testing, Validation, and Business Rules
@Test
void shouldThrowExceptionWhenEmailAlreadyExists() {
User user = new User(null, "John", "[email protected]");
when(userRepository.existsByEmail("[email protected]"))
.thenReturn(true);
Throwable exception = assertThrows(IllegalArgumentException.class, () -> {
userService.createUser(user);
});
assertNotNull(exception);
verify(userRepository, never()).save(any());
}
✔️ Ensures business rule enforcement
✔️ Verifies no unwanted interactions
8. Testing Exception Handling
@Test
void shouldThrowExceptionWhenUserNotFound() {
when(userRepository.findById(1L))
.thenReturn(Optional.empty());
Throwable exception = assertThrows(RuntimeException.class, () -> {
userService.getUserById(1L);
});
assertNotNull(exception);
}
9. Using Argument Matchers
Mockito provides argument matchers for flexibility.
when(userRepository.save(any(User.class)))
.thenReturn(user);
Common matchers:
-
any() -
eq() -
isNull() -
notNull()
⚠️ Rule: If you use a matcher for one argument, use matchers for all.
10. Arrange – Act – Assert Pattern (AAA)
Refactor tests for clarity:
@Test
void shouldReturnUserById() {
// Arrange
User user = new User(1L, "John", "[email protected]");
when(userRepository.findById(1L))
.thenReturn(Optional.of(user));
// Act
User result = userService.getUserById(1L);
// Assert
assertEquals("John", result.getName());
}
11. Common Mistakes in Service Layer Testing
❌ Mocking the service itself
❌ Using @Autowired in unit tests
❌ Using real repositories
❌ Overusing verify() instead of assertions
Unit Testing REST Controllers in Spring Boot
REST controllers handle HTTP requests and responses. When unit testing controllers, the goal is to verify request mapping, input validation, HTTP status codes, and response bodies—without starting a full server or loading the entire Spring context.
Spring Boot provides excellent tools for this through @WebMvcTest and MockMvc.
1. What Should Controller Unit Tests Cover?
Controller tests should verify:
-
Correct URL mapping and HTTP methods
-
Request/response JSON structure
-
HTTP status codes
-
Interaction with the service layer
They should not:
-
Call real services or repositories
-
Access a database
-
Test business logic (that belongs in service tests)
2. Sample REST Controller
Let’s create a simple UserController.
package com.example.demo.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.entity.User;
import com.example.demo.service.UserService;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.createUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return ResponseEntity.ok(user);
}
}
3. Using @WebMvcTest
@WebMvcTest loads only the web layer (controllers, filters, converters).
package com.example.demo.controller;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;
import com.example.demo.service.UserService;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Mock
private UserService userService;
}
Why this works:
-
MockMvcsimulates HTTP requests -
@MockBeaninjects a mock into the Spring context -
No full application startup
4. Testing GET Endpoints
@Test
void shouldReturnUserById() throws Exception {
User user = new User(1L, "John", "[email protected]");
when(userService.getUserById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"))
.andExpect(jsonPath("$.email").value("[email protected]"));
}
✔️ Verifies status
✔️ Verifies JSON fields
✔️ No real service call
5. Testing POST Endpoints
@Test
void shouldCreateUser() throws Exception {
User user = new User(null, "John", "[email protected]");
User savedUser = new User(1L, "John", "[email protected]");
when(userService.createUser(any(User.class)))
.thenReturn(savedUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John",
"email": "[email protected]"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John"));
}
6. Testing Validation Errors (Optional)
If your controller uses validation:
@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody User user) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(userService.createUser(user));
}
Test invalid input:
@Test
void shouldReturnBadRequestForInvalidInput() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{ "email": "invalid" }
"""))
.andExpect(status().isBadRequest());
}
7. Verifying Service Interaction
@Test
void shouldCallServiceWhenCreatingUser() throws Exception {
when(userService.createUser(any()))
.thenReturn(new User(1L, "John", "[email protected]"));
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John",
"email": "[email protected]"
}
"""))
.andExpect(status().isCreated());
verify(userService).createUser(any());
}
8. @WebMvcTest vs @SpringBootTest
| Annotation | Use Case |
|---|---|
@WebMvcTest |
Unit testing controllers |
@SpringBootTest |
Integration testing |
@MockBean |
Mock Spring-managed beans |
@Mock |
Plain Mockito tests |
👉 Use @WebMvcTest whenever possible for controller tests.
9. Common Controller Testing Mistakes
❌ Using @SpringBootTest for simple controller tests
❌ Forgetting @MockBean for service dependencies
❌ Testing business logic in controllers
❌ Skipping JSON response validation
MockMvc for Controller Testing (GET, POST, PUT, DELETE)
MockMvc allows you to test Spring MVC controllers by simulating HTTP requests and validating responses—without starting a real server. In this section, we’ll cover how to test all common HTTP methods: GET, POST, PUT, and DELETE.
1. What Is MockMvc?
MockMvc is part of Spring Test and provides:
-
A fluent API for performing HTTP requests
-
Full support for JSON request/response validation
-
Integration with
@WebMvcTest
It is ideal for controller unit tests.
2. Sample REST Controller (CRUD)
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.getUserById(id));
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(userService.createUser(user));
}
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@RequestBody User user) {
return ResponseEntity.ok(userService.updateUser(id, user));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
3. Test Class Setup
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
}
4. Testing GET Requests
@Test
void shouldGetUserById() throws Exception {
User user = new User(1L, "John", "[email protected]");
when(userService.getUserById(1L)).thenReturn(user);
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("John"));
}
✔️ Verifies HTTP 200
✔️ Validates JSON fields
5. Testing POST Requests
@Test
void shouldCreateUser() throws Exception {
User savedUser = new User(1L, "John", "[email protected]");
when(userService.createUser(any(User.class)))
.thenReturn(savedUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John",
"email": "[email protected]"
}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1));
}
✔️ Verifies HTTP 201
✔️ Tests request body parsing
6. Testing PUT Requests
@Test
void shouldUpdateUser() throws Exception {
User updatedUser = new User(1L, "John Updated", "[email protected]");
when(userService.updateUser(eq(1L), any(User.class)))
.thenReturn(updatedUser);
mockMvc.perform(put("/api/users/1")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John Updated",
"email": "[email protected]"
}
"""))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John Updated"));
}
✔️ Verifies HTTP 200
✔️ Tests path variables + request body
7. Testing DELETE Requests
@Test
void shouldDeleteUser() throws Exception {
doNothing().when(userService).deleteUser(1L);
mockMvc.perform(delete("/api/users/1"))
.andExpect(status().isNoContent());
verify(userService).deleteUser(1L);
}
✔️ Verifies HTTP 204
✔️ Confirms service interaction
8. Testing Error Responses
Example: user not found.
@Test
void shouldReturnNotFoundWhenUserDoesNotExist() throws Exception {
when(userService.getUserById(1L))
.thenThrow(new RuntimeException("User not found"));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isInternalServerError());
}
💡 In real apps, map exceptions to HTTP codes using
@ControllerAdvice.
9. Standalone MockMvc Setup (Without Spring Context)
For pure unit tests:
@BeforeEach
void setup() {
mockMvc = MockMvcBuilders
.standaloneSetup(new UserController(userService))
.build();
}
✔️ Fastest execution
✔️ No Spring context
10. Common MockMvc Pitfalls
❌ Forgetting contentType(MediaType.APPLICATION_JSON)
❌ Not testing response JSON
❌ Using @SpringBootTest unnecessarily
❌ Ignoring error scenarios
Testing with @SpringBootTest (When and When Not to Use It)
So far, we’ve focused on fast, isolated unit tests using JUnit, Mockito, and @WebMvcTest. In this section, we’ll look at @SpringBootTest, which is used for integration-style tests in Spring Boot—and learn when it makes sense to use it and when it does not.
1. What Is @SpringBootTest?
@SpringBootTest tells Spring Boot to load the full application context, just like when the application starts normally.
This includes:
-
All
@Configurationclasses -
Controllers, services, repositories
-
DataSource and JPA configuration
-
Security configuration (if present)
@SpringBootTest
class ApplicationIntegrationTest {
}
👉 This makes @SpringBootTest powerful—but also slower.
2. When Should You Use @SpringBootTest?
Use @SpringBootTest when you want to test:
✅ Bean wiring and dependency injection
✅ Application context startup
✅ Multiple layers working together
✅ Real database behavior (often with H2 or Testcontainers)
✅ Security configuration
Example use cases:
-
Ensuring the app starts correctly
-
Verifying repository + service + controller integration
-
Testing configuration properties
3. When NOT to Use @SpringBootTest
Avoid @SpringBootTest for:
❌ Pure unit tests
❌ Service logic tests
❌ Controller request mapping tests
❌ Simple validation tests
Why?
-
Slower execution
-
Harder to debug
-
More brittle tests
👉 Rule of thumb:
If Mockito alone can test it, do NOT use @SpringBootTest.
4. Example: Full Context Test
@SpringBootTest
class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@Test
void contextLoads() {
assertNotNull(userService);
}
}
This test verifies that:
-
The Spring context starts successfully
-
UserServiceis correctly wired
5. Using @SpringBootTest with Web Environment
To start an embedded web server:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {
@LocalServerPort
private int port;
@Test
void shouldStartServer() {
assertTrue(port > 0);
}
}
Useful when testing:
-
Real HTTP calls
-
Filters and interceptors
-
Serialization/deserialization end-to-end
6. @SpringBootTest with MockMvc
You can combine @SpringBootTest with MockMvc:
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldReturnOk() throws Exception {
mockMvc.perform(get("/actuator/health"))
.andExpect(status().isOk());
}
}
✔️ Loads full context
✔️ No real HTTP server
❌ Slower than @WebMvcTest
7. Replacing Beans with @MockBean
Even with @SpringBootTest, you can mock dependencies:
@SpringBootTest
class UserServiceTest {
@MockBean
private UserRepository userRepository;
@Autowired
private UserService userService;
}
✔️ Real Spring context
✔️ Controlled dependencies
8. Comparison: Testing Annotations
| Annotation | Scope | Speed | Use Case |
|---|---|---|---|
| JUnit + Mockito | Single class | 🚀 Fast | Pure unit tests |
@WebMvcTest |
Controller layer | ⚡ Fast | REST controllers |
@SpringBootTest |
Full context | 🐢 Slow | Integration tests |
9. Common Mistakes with @SpringBootTest
❌ Using it for every test
❌ Mixing unit and integration tests
❌ Not isolating slow tests
❌ Ignoring test profiles
10. Best Practices
✔️ Keep unit tests and integration tests separate
✔️ Name integration tests clearly (*IT, *IntegrationTest)
✔️ Use @ActiveProfiles("test")
✔️ Combine with Testcontainers for databases
Best Practices for Spring Boot Unit Testing
Writing tests is not just about coverage—it’s about creating fast, reliable, and maintainable tests that help you ship better software with confidence. In this section, we’ll summarize proven best practices for unit testing Spring Boot applications using JUnit 5, Mockito, and Spring Test utilities.
1. Prefer Unit Tests Over Integration Tests
Unit tests should be your first line of defense.
✔️ Fast execution
✔️ Easy to debug
✔️ No infrastructure required
Use integration tests (@SpringBootTest) sparingly and only when necessary.
80/20 rule:
~80% unit tests, ~20% integration tests
2. Test One Thing at a Time
Each test should validate a single behavior.
❌ Bad:
void testCreateAndDeleteUser() { ... }
✔️ Good:
void shouldCreateUserSuccessfully() { ... }
void shouldDeleteUserById() { ... }
Clear intent = easier maintenance.
3. Follow the Arrange–Act–Assert (AAA) Pattern
Structure every test clearly:
@Test
void shouldReturnUserById() {
// Arrange
when(userRepository.findById(1L))
.thenReturn(Optional.of(user));
// Act
User result = userService.getUserById(1L);
// Assert
assertEquals("John", result.getName());
}
This improves readability and consistency.
4. Use Descriptive Test Names
Test names should describe behavior, not implementation.
Recommended patterns:
-
shouldReturnXWhenCondition -
givenCondition_whenAction_thenResult
Example:
shouldThrowExceptionWhenEmailAlreadyExists()
5. Avoid Loading the Spring Context Unnecessarily
❌ Don’t use:
-
@SpringBootTest -
@Autowired
✔️ Prefer:
-
Plain JUnit + Mockito
-
Constructor injection
-
@WebMvcTestfor controllers
Context loading slows tests and increases flakiness.
6. Mock Dependencies, Not the Class Under Test
✔️ Mock:
-
Repositories
-
External services
-
Clients (REST, messaging)
❌ Do NOT mock:
-
The service you’re testing
-
DTOs or entities
Your goal is to test real logic.
7. Verify Behavior, Not Implementation Details
❌ Overusing verify():
verify(repository).findById(1L);
verify(repository).save(any());
✔️ Focus on results:
assertEquals("John", result.getName());
Use verify() only when interaction matters.
8. Keep Tests Independent
Each test must:
-
Be runnable alone
-
Not dependent on the test order
-
Clean up after itself
Avoid shared mutable state.
9. Use Test Profiles
Always separate test configuration:
@ActiveProfiles("test")
Use:
-
In-memory DB (H2)
-
Test-specific properties
-
Disabled schedulers or external calls
10. Organize Tests by Layer
Mirror your main package structure:
src/test/java
├── controller
├── service
└── repository
This improves discoverability and clarity.
11. Use Parameterized Tests for Edge Cases
Reduce duplication with JUnit 5:
@ParameterizedTest
@ValueSource(ints = {0, -1, -5})
void shouldRejectInvalidIds(int id) {
assertThrows(IllegalArgumentException.class, () -> {
service.getUserById(id);
});
}
12. Run Tests Automatically
✔️ Run tests on every build
✔️ Integrate with CI/CD
✔️ Fail fast on broken tests
mvn test
13. Common Anti-Patterns to Avoid
❌ Writing tests after bugs appear
❌ Massive test classes
❌ Asserting logs instead of behavior
❌ Ignoring failing tests
Tests should be trusted, not ignored.
14. Final Checklist
Before committing your tests, ask:
-
Are tests fast?
-
Are they readable?
-
Do they test behavior?
-
Are they isolated?
-
Can a new developer understand them?
Conclusion and Next Steps
Unit testing is a fundamental skill for building robust, maintainable, and scalable Spring Boot applications. By combining JUnit 5 and Mockito, you can confidently test your application logic without relying on slow or fragile external dependencies.
In this tutorial, you learned how to:
-
Write pure unit tests using JUnit 5
-
Mock dependencies effectively with Mockito
-
Test service-layer business logic in isolation
-
Validate REST controllers using
@WebMvcTestand MockMvc -
Understand when to use (and avoid)
@SpringBootTest -
Apply best practices for fast, reliable, and readable tests
By following these patterns, your tests will run faster, fail less often, and provide meaningful feedback during development and CI/CD pipelines.
Next Steps
To continue improving your testing skills in Spring Boot, consider exploring:
-
Integration Testing with Testcontainers
Test real databases and infrastructure safely using Docker-based containers. -
Spring Security Testing
Learn how to test secured endpoints with MockMvc and JWT authentication. -
Repository Testing with @DataJpaTest
Validate JPA queries and database mappings efficiently. -
Contract Testing
Use tools like Spring Cloud Contract to test service-to-service communication. -
Test Coverage and Quality Gates
Integrate JaCoCo and enforce coverage rules in CI pipelines.
Final Recommendation
Start small:
-
Write unit tests for every new service
-
Add controller tests for public APIs
-
Introduce integration tests only when needed
This layered approach keeps your test suite fast, focused, and maintainable—exactly what modern Spring Boot projects need.
You can find the full source code on our GitHub.
That's just the basics. If you need more deep learning about Spring Boot, you can take the following cheap course:
- [NEW] Spring Boot 3, Spring 6 & Hibernate for Beginners
- Java Spring Framework 6, Spring Boot 3, Spring AI Telusko
- [NEW] Master Microservices with SpringBoot,Docker,Kubernetes
- Spring Boot Microservices Professional eCommerce Masterclass
- Java Spring Boot: Professional eCommerce Project Masterclass
- Master Backend Development using Java & Spring Boot
- Spring Boot REST APIs: Building Modern APIs with Spring Boot
- [NEW] Spring Boot 3, Spring Framework 6: Beginner to Guru
- Spring 6 & Spring Boot 3: AI, Security, Docker, Cloud
- From Java Dev to AI Engineer: Spring AI Fast Track
Thanks!
