unit-testing
Generate comprehensive unit and integration tests for Java/Spring Boot code using JUnit 5, Mockito, and Spring Boot Test. Use this skill whenever the user wants to write tests, improve test coverage,
SKILL.mdskills/unit-testing/SKILL.md
GitHubJava Unit Testing Skill
Generate thorough, maintainable tests for Java/Spring Boot following the AAA pattern, proper mocking strategies, and meaningful assertion coverage.
Testing Strategy by Layer
Service Layer → Unit Tests (Pure JUnit 5 + Mockito)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserServiceImpl userService;
// Tests here
}
Controller Layer → Slice Tests (@WebMvcTest)
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
// Tests here
}
Repository Layer → Slice Tests (@DataJpaTest)
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
// Tests here - uses in-memory H2
}
Integration Tests → @SpringBootTest
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class UserIntegrationTest {
@Autowired
private MockMvc mockMvc;
// Full stack tests
}
Test Method Naming Convention
Use the pattern: methodName_stateUnderTest_expectedBehavior
findById_existingId_returnsUser()
findById_nonExistentId_throwsResourceNotFoundException()
createUser_duplicateEmail_throwsConflictException()
createUser_validRequest_persistsAndReturnsResponse()
deleteUser_adminRole_deletesSuccessfully()
deleteUser_insufficientRole_throwsForbiddenException()
AAA Pattern — Always Follow This Structure
@Test
void findById_existingId_returnsUser() {
// ARRANGE
Long userId = 1L;
User mockUser = User.builder().id(userId).name("Alice").build();
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
// ACT
UserResponse result = userService.findById(userId);
// ASSERT
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(userId);
assertThat(result.getName()).isEqualTo("Alice");
verify(userRepository, times(1)).findById(userId);
}
Coverage Checklist
For every method, generate tests for:
- ✅ Happy path — normal input, expected output
- ✅ Not found — entity doesn't exist → exception thrown
- ✅ Invalid input — null, empty, out of range
- ✅ Duplicate / conflict — unique constraint violation
- ✅ Boundary values — min/max for numeric fields, empty list vs null list
- ✅ Side effects — verify
save(),delete(), external service calls - ✅ Exception propagation — when dependency throws, service propagates correctly
Mockito Patterns
Stubbing
when(repo.findById(1L)).thenReturn(Optional.of(entity));
when(repo.save(any(User.class))).thenAnswer(inv -> inv.getArgument(0));
doThrow(new DataIntegrityViolationException("...")).when(repo).save(any());
Verification
verify(repo, times(1)).save(any(User.class));
verify(emailService, never()).sendWelcomeEmail(any());
verifyNoMoreInteractions(repo);
ArgumentCaptor
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(repo).save(captor.capture());
assertThat(captor.getValue().getEmail()).isEqualTo("alice@test.com");
MockMvc Patterns for Controller Tests
@Test
void createUser_validRequest_returns201() throws Exception {
CreateUserRequest request = new CreateUserRequest("Alice", "alice@test.com");
UserResponse response = new UserResponse(1L, "Alice", "alice@test.com");
when(userService.create(any())).thenReturn(response);
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.name").value("Alice"));
}
@Test
void createUser_invalidEmail_returns400() throws Exception {
CreateUserRequest request = new CreateUserRequest("Alice", "not-an-email");
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").exists());
}
Assertions — Prefer AssertJ over JUnit Assertions
// Prefer AssertJ
assertThat(result).isNotNull();
assertThat(result.getName()).isEqualTo("Alice");
assertThat(list).hasSize(3).extracting(User::getName).containsExactly("A", "B", "C");
// For exceptions
assertThatThrownBy(() -> userService.findById(999L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessageContaining("999");
Test Data Builders
Always suggest a test builder/factory for entities used across multiple tests:
public class UserTestFactory {
public static User.UserBuilder defaultUser() {
return User.builder()
.id(1L)
.name("Test User")
.email("test@example.com")
.active(true)
.createdAt(LocalDateTime.now());
}
}