Testing patterns for Spring Boot including MockMvc, Testcontainers, mocking, and test structure.
npx claudepluginhub sumanpapanaboina1983/adlc-accelerator-kit-pluginsThis skill uses the workspace's default tool permissions.
```
Provides Spring Boot testing patterns for unit (Mockito), slice (@DataJpaTest/@WebMvcTest), integration (@SpringBootTest), and Testcontainers-based tests with JUnit 5. Use when writing @Test methods, @MockBean mocks, or test suites.
Guides test-driven development for Spring Boot using JUnit 5, Mockito, MockMvc, Testcontainers, and JaCoCo. Use when adding features, fixing bugs, or refactoring.
Guides TDD for Spring Boot with JUnit 5, Mockito, MockMvc, Testcontainers, JaCoCo. Covers unit, web, integration, persistence tests for features, bugs, refactors.
Share bugs, ideas, or general feedback.
should_[expectedBehavior]_when[condition]()
Examples:
should_returnAllTodos_whenGetAll()should_return404_whenTodoNotFound()should_return400_whenTitleIsBlank()should_createTodo_whenValidRequest()@WebMvcTest(TodoController.class)
class TodoControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private TodoService todoService;
@Test
void should_returnAllTodos_whenGetAll() throws Exception {
// Given
List<TodoResponse> todos = List.of(
new TodoResponse(1L, "Test Todo", false, Instant.now())
);
when(todoService.findAll()).thenReturn(todos);
// When & Then
mockMvc.perform(get("/api/v1/todos"))
.andExpect(status().isOk())
.andExpect(jsonPath("$", hasSize(1)))
.andExpect(jsonPath("$[0].title").value("Test Todo"));
}
@Test
void should_return201_whenCreateValidTodo() throws Exception {
// Given
CreateTodoRequest request = new CreateTodoRequest("New Todo");
TodoResponse response = new TodoResponse(1L, "New Todo", false, Instant.now());
when(todoService.create(any())).thenReturn(response);
// When & Then
mockMvc.perform(post("/api/v1/todos")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.title").value("New Todo"));
}
@Test
void should_return400_whenTitleIsBlank() throws Exception {
// Given
CreateTodoRequest request = new CreateTodoRequest("");
// When & Then
mockMvc.perform(post("/api/v1/todos")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.error").value("Validation Failed"));
}
@Test
void should_return404_whenTodoNotFound() throws Exception {
// Given
when(todoService.findById(999L)).thenThrow(new TodoNotFoundException(999L));
// When & Then
mockMvc.perform(get("/api/v1/todos/999"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.error").value("Not Found"));
}
@Test
void should_return204_whenDeleteExistingTodo() throws Exception {
// Given
doNothing().when(todoService).delete(1L);
// When & Then
mockMvc.perform(delete("/api/v1/todos/1"))
.andExpect(status().isNoContent());
}
}
@ExtendWith(MockitoExtension.class)
class TodoServiceTest {
@Mock
private TodoRepository todoRepository;
@InjectMocks
private TodoService todoService;
@Test
void should_returnAllTodos_whenFindAll() {
// Given
List<Todo> todos = List.of(createTodo(1L, "Test"));
when(todoRepository.findAll()).thenReturn(todos);
// When
List<TodoResponse> result = todoService.findAll();
// Then
assertThat(result).hasSize(1);
assertThat(result.get(0).title()).isEqualTo("Test");
}
@Test
void should_createTodo_whenValidRequest() {
// Given
CreateTodoRequest request = new CreateTodoRequest("New Todo");
when(todoRepository.save(any())).thenAnswer(invocation -> {
Todo todo = invocation.getArgument(0);
todo.setId(1L);
return todo;
});
// When
TodoResponse result = todoService.create(request);
// Then
assertThat(result.id()).isEqualTo(1L);
assertThat(result.title()).isEqualTo("New Todo");
assertThat(result.completed()).isFalse();
}
@Test
void should_throwException_whenTodoNotFound() {
// Given
when(todoRepository.findById(999L)).thenReturn(Optional.empty());
// When & Then
assertThatThrownBy(() -> todoService.findById(999L))
.isInstanceOf(TodoNotFoundException.class)
.hasMessageContaining("999");
}
private Todo createTodo(Long id, String title) {
Todo todo = new Todo();
todo.setId(id);
todo.setTitle(title);
todo.setCompleted(false);
todo.setCreatedAt(Instant.now());
return todo;
}
}
@DataJdbcTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class TodoRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@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 TodoRepository todoRepository;
@Test
void should_saveTodo_whenValidData() {
// Given
Todo todo = new Todo();
todo.setTitle("Test Todo");
todo.setCompleted(false);
todo.setCreatedAt(Instant.now());
// When
Todo saved = todoRepository.save(todo);
// Then
assertThat(saved.getId()).isNotNull();
assertThat(saved.getTitle()).isEqualTo("Test Todo");
}
@Test
void should_findById_whenExists() {
// Given
Todo todo = new Todo();
todo.setTitle("Findable Todo");
todo.setCompleted(false);
todo.setCreatedAt(Instant.now());
Todo saved = todoRepository.save(todo);
// When
Optional<Todo> found = todoRepository.findById(saved.getId());
// Then
assertThat(found).isPresent();
assertThat(found.get().getTitle()).isEqualTo("Findable Todo");
}
@Test
void should_returnEmpty_whenNotExists() {
// When
Optional<Todo> found = todoRepository.findById(99999L);
// Then
assertThat(found).isEmpty();
}
}
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class TodoIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb");
@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;
@Test
void should_createAndRetrieveTodo() {
// Create
CreateTodoRequest request = new CreateTodoRequest("Integration Test Todo");
ResponseEntity<TodoResponse> createResponse = restTemplate.postForEntity(
"/api/v1/todos",
request,
TodoResponse.class
);
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(createResponse.getBody()).isNotNull();
Long todoId = createResponse.getBody().id();
// Retrieve
ResponseEntity<TodoResponse> getResponse = restTemplate.getForEntity(
"/api/v1/todos/" + todoId,
TodoResponse.class
);
assertThat(getResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(getResponse.getBody().title()).isEqualTo("Integration Test Todo");
}
}
when(repository.findById(1L)).thenReturn(Optional.of(todo));
when(repository.findAll()).thenReturn(List.of(todo1, todo2));
when(repository.save(any(Todo.class))).thenAnswer(invocation -> {
Todo t = invocation.getArgument(0);
t.setId(1L);
return t;
});
doNothing().when(repository).deleteById(1L);
doThrow(new DataAccessException("DB error") {}).when(repository).deleteById(999L);
verify(repository).save(any(Todo.class));
verify(repository, times(1)).findById(1L);
verify(repository, never()).deleteById(any());
@Captor
ArgumentCaptor<Todo> todoCaptor;
@Test
void should_saveTodoWithCorrectData() {
// When
todoService.create(new CreateTodoRequest("Test"));
// Then
verify(todoRepository).save(todoCaptor.capture());
Todo captured = todoCaptor.getValue();
assertThat(captured.getTitle()).isEqualTo("Test");
assertThat(captured.isCompleted()).isFalse();
}
class TodoTestBuilder {
private Long id = 1L;
private String title = "Default Title";
private boolean completed = false;
private Instant createdAt = Instant.now();
public static TodoTestBuilder aTodo() {
return new TodoTestBuilder();
}
public TodoTestBuilder withId(Long id) {
this.id = id;
return this;
}
public TodoTestBuilder withTitle(String title) {
this.title = title;
return this;
}
public TodoTestBuilder completed() {
this.completed = true;
return this;
}
public Todo build() {
Todo todo = new Todo();
todo.setId(id);
todo.setTitle(title);
todo.setCompleted(completed);
todo.setCreatedAt(createdAt);
return todo;
}
}
// Usage
Todo todo = aTodo().withTitle("Test").completed().build();
Add this to your <build><plugins> section to enforce 80% line coverage:
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<!-- Prepare agent for coverage collection -->
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<!-- Generate coverage report -->
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<!-- Enforce coverage thresholds -->
<execution>
<id>check</id>
<phase>verify</phase>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.70</minimum>
</limit>
</limits>
</rule>
<!-- Service layer must have higher coverage -->
<rule>
<element>PACKAGE</element>
<includes>
<include>**/service/**</include>
</includes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.85</minimum>
</limit>
</limits>
</rule>
</rules>
<!-- Exclude from coverage -->
<excludes>
<exclude>**/*Application.class</exclude>
<exclude>**/*Config.class</exclude>
<exclude>**/dto/**</exclude>
<exclude>**/entity/**</exclude>
</excludes>
</configuration>
</execution>
</executions>
</plugin>
# Run tests with coverage
mvn test jacoco:report
# View coverage report
open target/site/jacoco/index.html
# Verify coverage meets thresholds (fails build if below 80%)
mvn verify
# Check specific package coverage
cat target/site/jacoco/jacoco.xml | grep -A5 "package name=\"com/example/service\""
| Report | Path |
|---|---|
| HTML Report | target/site/jacoco/index.html |
| XML Report | target/site/jacoco/jacoco.xml |
| CSV Report | target/site/jacoco/jacoco.csv |
Always exclude:
*Application.class)*Config.class)| Issue | Solution |
|---|---|
| Coverage < 80% | Add more unit tests, especially for edge cases |
| Service methods uncovered | Mock dependencies, test all branches |
| Exception handlers uncovered | Write tests that trigger exceptions |
| Validation logic uncovered | Test with invalid inputs |