About spring-boot-test
A reference skill for testing Spring Boot applications, aimed at developers writing tests across the application context, web, data, and serialization layers. It explains test annotations, mocking, configuration, assertions, and common pitfalls.
Key features
- 01Full-context tests with @SpringBootTest
- 02Focused slice tests for web and data
- 03MockMvc request and authentication tests
- 04Bean mocking with @MockBean and @SpyBean
- 05Test properties and custom configuration
- 06AssertJ assertions and troubleshooting
Use cases
- 01Test controllers without loading the full application
- 02Verify JPA repositories with slice tests
- 03Replace external dependencies with mocks
- 04Diagnose slow or misconfigured tests
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ claude-dev-suite/ claude-dev-suite --skill spring-boot-testAfter installing, type/spring-boot-testin Claude Code to confirm it loaded.
Bundled filesquick-ref/annotations.md
Details
From the project READMESpring Boot Testing
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:spring-boot-testfor comprehensive documentation.
When NOT to Use This Skill
- Pure Unit Tests - Use
junitwith Mockito for faster tests without Spring context - Integration Tests with Real Database - Use
spring-boot-integrationwith Testcontainers - REST API Client Testing - Use
rest-assuredfor HTTP testing - E2E Web Testing - Use Selenium or Playwright
- Microservice Contract Testing - Use Spring Cloud Contract
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Includes: JUnit 5, Mockito, AssertJ, Hamcrest, JSONPath, Spring Test
Test Annotations
@SpringBootTest - Full Context
@SpringBootTest
class ApplicationTest {
@Autowired
private UserService userService;
@Test
void contextLoads() {
assertThat(userService).isNotNull();
}
}
// With web environment
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class WebApplicationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void healthCheck() {
ResponseEntity<String> response = restTemplate
.getForEntity("/actuator/health", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}
Slice Tests (Faster, Focused)
| Annotation | Layer | Auto-configured |
|---|---|---|
@WebMvcTest | Controllers | MockMvc, Jackson |
@DataJpaTest | JPA Repositories | TestEntityManager, DataSource |
@DataMongoTest | MongoDB | MongoTemplate |
@JsonTest | JSON serialization | JacksonTester |
@RestClientTest | REST clients | MockRestServiceServer |
// Controller test - only loads web layer
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void getUser_ReturnsUser() throws Exception {
when(userService.findById(1L))
.thenReturn(Optional.of(new User(1L, "John")));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("John"));
}
}
// Repository test - uses embedded database
@DataJpaTest
class UserRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void findByEmail_ReturnsUser() {
User user = new User("[email protected]", "Test User");
entityManager.persistAndFlush(user);
Optional<User> found = repository.findByEmail("[email protected]");
assertThat(found).isPresent()
.hasValueSatisfying(u -> assertThat(u.getName()).isEqualTo("Test User"));
}
}
MockMvc
Basic Requests
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
// GET request
@Test
void getUsers() throws Exception {
when(userService.findAll()).thenReturn(List.of(
new User(1L, "Alice"),
new User(2L, "Bob")
));
mockMvc.perform(get("/api/users")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$", hasSize(2)))
.andExpect(jsonPath("$[0].name").value("Alice"));
}
// POST request with JSON body
@Test
void createUser() throws Exception {
CreateUserRequest request = new CreateUserRequest("John", "[email protected]");
User createdUser = new User(1L, "John", "[email protected]");
when(userService.create(any())).thenReturn(createdUser);
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"name": "John",
"email": "[email protected]"
}
"""))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.id").value(1));
}
// PUT request
@Test
void updateUser() throws Exception {
mockMvc.perform(put("/api/users/{id}", 1)
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"name": "Updated Name"}
"""))
.andExpect(status().isOk());
verify(userService).update(eq(1L), any());
}
// DELETE request
@Test
void deleteUser() throws Exception {
mockMvc.perform(delete("/api/users/{id}", 1))
.andExpect(status().isNoContent());
verify(userService).delete(1L);
}
}
With Authentication
@WebMvcTest(AdminController.class)
@Import(SecurityConfig.class)
class AdminControllerTest {
@Autowired
private MockMvc mockMvc;
// Using @WithMockUser
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_WithAdminRole_Succeeds() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_WithUserRole_Forbidden() throws Exception {
mockMvc.perform(get("/api/admin/dashboard"))
.andExpect(status().isForbidden());
}
// Using JWT token
@Test
void withJwtToken() throws Exception {
String token = jwtTokenProvider.createToken("admin", List.of("ROLE_ADMIN"));
mockMvc.perform(get("/api/admin/dashboard")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}
Mocking
@MockBean
@SpringBootTest
class OrderServiceTest {
@Autowired
private OrderService orderService;
@MockBean // Replaces bean in context with mock
private PaymentGateway paymentGateway;
@MockBean
private InventoryService inventoryService;
@Test
void placeOrder_WhenPaymentSucceeds_CreatesOrder() {
when(inventoryService.checkStock(any())).thenReturn(true);
when(paymentGateway.charge(any())).thenReturn(PaymentResult.success());
Order order = orderService.placeOrder(new OrderRequest(...));
assertThat(order.getStatus()).isEqualTo(OrderStatus.CONFIRMED);
verify(paymentGateway).charge(any());
}
@Test
void placeOrder_WhenPaymentFails_ThrowsException() {
when(paymentGateway.charge(any()))
.thenThrow(new PaymentException("Card declined"));
assertThatThrownBy(() -> orderService.placeOrder(new OrderRequest(...)))
.isInstanceOf(PaymentException.class)
.hasMessage("Card declined");
}
}
@SpyBean
@SpringBootTest
class NotificationServiceTest {
@Autowired
private NotificationService notificationService;
@SpyBean // Wraps real bean, allows partial mocking
private EmailSender emailSender;
@Test
void sendNotification_CallsEmailSender() {
notificationService.notify(user, "Hello");
verify(emailSender).send(eq(user.getEmail()), any());
}
@Test
void sendNotification_WhenEmailFails_LogsError() {
doThrow(new EmailException("SMTP error"))
.when(emailSender).send(any(), any());
// Method should handle exception gracefully
assertThatCode(() -> notificationService.notify(user, "Hello"))
.doesNotThrowAnyException();
}
}
Test Configuration
Test Properties
@SpringBootTest
@TestPropertySource(properties = {
"app.feature.enabled=true",
"app.external.url=http://localhost:8080"
})
class FeatureTest { }
// Or use test profile
@SpringBootTest
@ActiveProfiles("test")
class ProfileTest { }
application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create-drop
app:
external:
url: http://localhost:${wiremock.server.port}
Custom Test Configuration
@TestConfiguration
public class TestConfig {
@Bean
@Primary
public Clock testClock() {
return Clock.fixed(
Instant.parse("2025-01-15T10:00:00Z"),
ZoneId.of("UTC")
);
}
@Bean
@Primary
public PaymentGateway testPaymentGateway() {
return new FakePaymentGateway();
}
}
@SpringBootTest
@Import(TestConfig.class)
class TimeBasedFeatureTest { }
AssertJ Assertions
// Basic assertions
assertThat(user.getName()).isEqualTo("John");
assertThat(user.getAge()).isGreaterThan(18);
assertThat(user.getEmail()).contains("@").endsWith(".com");
// Collection assertions
assertThat(users)
.hasSize(3)
.extracting(User::getName)
.containsExactly("Alice", "Bob", "Charlie");
// Exception assertions
assertThatThrownBy(() -> service.process(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("null");
// Optional assertions
assertThat(repository.findById(1L))
.isPresent()
.hasValueSatisfying(user ->
assertThat(user.getName()).isEqualTo("John")
);
// Soft assertions (collect all failures)
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(user.getName()).isEqualTo("John");
softly.assertThat(user.getEmail()).contains("@");
softly.assertThat(user.getAge()).isPositive();
});
Test Data Builders
public class UserTestBuilder {
private Long id = 1L;
private String name = "Test User";
private String email = "[email protected]";
private UserRole role = UserRole.USER;
public static UserTestBuilder aUser() {
return new UserTestBuilder();
}
public UserTestBuilder withId(Long id) {
this.id = id;
return this;
}
public UserTestBuilder withName(String name) {
this.name = name;
return this;
}
public UserTestBuilder withEmail(String email) {
this.email = email;
return this;
}
public UserTestBuilder withRole(UserRole role) {
this.role = role;
return this;
}
public UserTestBuilder asAdmin() {
this.role = UserRole.ADMIN;
return this;
}
public User build() {
return new User(id, name, email, role);
}
}
// Usage
User admin = aUser().withName("Admin").asAdmin().build();
User regularUser = aUser().build();
Anti-Patterns
| Anti-Pattern | Why It's Bad | Solution |
|---|---|---|
| Using @SpringBootTest for all tests | Extremely slow | Use slice tests (@WebMvcTest, @DataJpaTest) |
| Not using @MockBean | Testing real beans | Mock external dependencies |
| Hardcoding ports in tests | Port conflicts | Use @LocalServerPort with RANDOM_PORT |
| Testing private methods | Coupled to implementation | Test through controller/service API |
| Not isolating test data | Tests interfere | Use @Transactional or cleanup in @AfterEach |
| Ignoring @Sql scripts | Manual setup duplication | Use @Sql for test data setup |
| No test profiles | Polluting dev/prod config | Use @ActiveProfiles("test") |
Quick Troubleshooting
| Problem | Likely Cause | Solution |
|---|---|---|
| "Unable to find @SpringBootConfiguration" | Main class not found | Add @SpringBootTest(classes = App.class) |
| Test very slow | Using @SpringBootTest unnecessarily | Use slice tests (@WebMvcTest, etc.) |
| "No qualifying bean" | Missing @MockBean | Add @MockBean for dependencies |
| Port already in use | Hardcoded port | Use webEnvironment = RANDOM_PORT |
| "Could not autowire" | Bean not in test context | Check component scan or add @Import |
| Flaky test | Database state not reset | Use @Transactional or @DirtiesContext |
Reference
- Quick Reference: Annotations
- Spring Boot Testing Guide
- See also:
spring-boot-integrationfor Testcontainers
Comments
Markdown supported