Spring testing is strongest when each test loads only the boundary it needs. Plain unit tests verify Java logic without Spring, slice tests load one framework area such as MVC or JPA, and @SpringBootTest loads the full application context for integration behavior. The smallest sufficient scope produces faster feedback and clearer failures.
After this lesson, you can select a test scope, use MockMvc without a running server, decide when a real database is necessary, and avoid tests that pass only because mocks hide framework configuration errors.
| Annotation | Description | Use Case |
|---|---|---|
| @SpringBootTest | Loads the full application context | Integration tests |
| @WebMvcTest | Loads only the web layer (controllers) | Controller unit tests |
| @DataJpaTest | Loads only JPA components, uses in-memory DB | Repository tests |
| @MockBean | Creates a Mockito mock and adds it to the Spring context | Mocking dependencies |
| @ExtendWith(SpringExtension.class) | Integrates Spring with JUnit 5 | All Spring tests |
package com.example.controller;
import com.example.model.User;
import com.example.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
import java.util.Optional;
import static org.mockito.Mockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
// @WebMvcTest loads only the web layer - fast, no DB needed
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc; // Simulates HTTP requests without starting a server
@MockBean
private UserService userService; // Replaces real UserService with a Mockito mock
@Test
void getAllUsers_shouldReturn200WithUserList() throws Exception {
// Arrange: define mock behavior
when(userService.findAll()).thenReturn(
List.of(new User(1L, "Alice", "alice@example.com"),
new User(2L, "Bob", "bob@example.com"))
);
// Act & Assert: perform GET and verify response
mockMvc.perform(get("/api/users")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2))
.andExpect(jsonPath("$[0].name").value("Alice"))
.andExpect(jsonPath("$[1].email").value("bob@example.com"));
verify(userService, times(1)).findAll();
}
@Test
void getUserById_whenNotFound_shouldReturn404() throws Exception {
when(userService.findById(99L)).thenReturn(Optional.empty());
mockMvc.perform(get("/api/users/99"))
.andExpect(status().isNotFound());
}
@Test
void createUser_withValidData_shouldReturn201() throws Exception {
User newUser = new User(null, "Charlie", "charlie@example.com");
User saved = new User(3L, "Charlie", "charlie@example.com");
when(userService.save(any(User.class))).thenReturn(saved);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Charlie\",\"email\":\"charlie@example.com\"}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(3))
.andExpect(jsonPath("$.name").value("Charlie"));
}
}
package com.example.controller;
import com.example.model.User;
import com.example.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User saved = userService.save(user);
return ResponseEntity.status(201).body(saved);
}
}
package com.example.service;
import com.example.model.User;
import com.example.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
// Pure Mockito test - no Spring context loaded (very fast)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository; // Mockito mock
@InjectMocks
private UserService userService; // Injects the mock into UserService
@Test
void findById_whenUserExists_shouldReturnUser() {
User user = new User(1L, "Alice", "alice@example.com");
when(userRepository.findById(1L)).thenReturn(Optional.of(user));
Optional<User> result = userService.findById(1L);
assertThat(result).isPresent();
assertThat(result.get().getName()).isEqualTo("Alice");
verify(userRepository).findById(1L);
}
@Test
void save_withDuplicateEmail_shouldThrowException() {
User user = new User(null, "Bob", "bob@example.com");
when(userRepository.existsByEmail("bob@example.com")).thenReturn(true);
assertThatThrownBy(() -> userService.save(user))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Email already exists");
verify(userRepository, never()).save(any());
}
}
package com.example.repository;
import com.example.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
// @DataJpaTest: loads JPA layer only, uses H2 in-memory DB by default
// Each test runs in a transaction that is rolled back after the test
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager; // Helper for persisting test data
@Autowired
private UserRepository userRepository;
@Test
void findByEmail_whenExists_shouldReturnUser() {
// Arrange: persist a user directly via EntityManager
User user = new User(null, "Alice", "alice@example.com");
entityManager.persistAndFlush(user);
// Act
Optional<User> found = userRepository.findByEmail("alice@example.com");
// Assert
assertThat(found).isPresent();
assertThat(found.get().getName()).isEqualTo("Alice");
}
@Test
void existsByEmail_whenNotExists_shouldReturnFalse() {
boolean exists = userRepository.existsByEmail("nobody@example.com");
assertThat(exists).isFalse();
}
@Test
void save_shouldPersistAndGenerateId() {
User user = new User(null, "Bob", "bob@example.com");
User saved = userRepository.save(user);
assertThat(saved.getId()).isNotNull();
assertThat(saved.getId()).isGreaterThan(0);
}
}
package com.example;
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.boot.test.web.server.LocalServerPort;
import org.springframework.http.*;
import static org.assertj.core.api.Assertions.*;
// @SpringBootTest: loads full context, starts embedded server on random port
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTest {
@LocalServerPort
private int port; // Injected random port
@Autowired
private TestRestTemplate restTemplate; // HTTP client for integration tests
@Test
void getUsers_shouldReturnOk() {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/users", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void createUser_shouldReturn201() {
String body = "{\"name\":\"Test\",\"email\":\"test@example.com\"}";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
ResponseEntity<String> response = restTemplate.postForEntity(
"http://localhost:" + port + "/api/users",
new HttpEntity<>(body, headers), String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
}
Start with a plain JUnit test for calculations, validation rules, and service decisions whose collaborators can be passed directly. Use @WebMvcTest when the contract under test is request mapping, binding, validation, serialization, or status handling. Use @DataJpaTest for mappings and repository queries. Reserve @SpringBootTest for workflows that genuinely require multiple configured layers.
A slice test intentionally omits unrelated beans. Provide required collaborators with test replacements, but do not mock the object whose framework behavior you are trying to verify. If every dependency becomes a mock, the test may prove your stubbing rather than the application contract.
MockMvc sends a mock HTTP request through DispatcherServlet and the configured Spring MVC infrastructure without starting a network server. Assert externally meaningful behavior: status, headers, response JSON, view name, model state, and validation errors. Avoid asserting private controller methods or incidental implementation calls.
For security-enabled controllers, include spring-security-test and state the caller, authority, and CSRF token explicitly. A controller test that disables the filter chain can miss the most important part of the endpoint contract.
An in-memory database is useful for fast mapping feedback but can differ from production in SQL syntax, constraints, locking, and query planning. Run important repository and migration tests against the same database engine used in production, commonly through an isolated container or dedicated test service.
Keep test data small and intentional. Build only the rows needed for the behavior, assert the observable result, and let each test control its transaction assumptions. A passing test should not depend on method order or records left by another test.
Try this next
0 of 2 completed
Explore 500+ free tutorials across 20+ languages and frameworks.