Guide for writing comprehensive tests for Spring Boot applications including unit tests, integration tests, and test slices. Use this when creating tests for new features or fixing bugs.
Follow these practices for comprehensive test coverage.
src/
├── test/java/com/salonhub/api/ # Unit tests
│ └── [domain]/
│ ├── controller/
│ │ └── [Feature]ControllerTest.java
│ ├── service/
│ │ └── [Feature]ServiceTest.java
│ └── repository/
│ └── [Feature]RepositoryTest.java
├── integration/java/com/salonhub/api/ # Integration tests
│ └── [domain]/
│ └── [Feature]IntegrationTest.java
└── testFixtures/java/com/salonhub/api/ # Shared test utilities
└── [domain]/
├── [Domain]DatabaseDefault.java
└── [Domain]TestDataBuilder.java
@WebMvcTest(controllers = AppointmentController.class)
@Import(TestSecurityConfig.class)
class AppointmentControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private AppointmentService appointmentService;
@Autowired
private ObjectMapper objectMapper;
@Test
@WithMockUser(roles = {"FRONT_DESK"})
void getAppointment_whenExists_shouldReturn200() throws Exception {
// Arrange
AppointmentResponseDTO dto = new AppointmentResponseDTO(1L, "John", "Alice", "Haircut");
when(appointmentService.findById(1L)).thenReturn(dto);
// Act & Assert
mockMvc.perform(get("/api/appointments/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.customerName").value("John"));
verify(appointmentService).findById(1L);
}
@Test
@WithMockUser(roles = {"FRONT_DESK"})
void createAppointment_withValidRequest_shouldReturn201() throws Exception {
// Arrange
AppointmentRequestDTO request = new AppointmentRequestDTO();
request.setCustomerId(1L);
request.setEmployeeId(1L);
AppointmentResponseDTO response = new AppointmentResponseDTO(1L, "John", "Alice", "Haircut");
when(appointmentService.create(any())).thenReturn(response);
// Act & Assert
mockMvc.perform(post("/api/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(1));
}
@Test
@WithMockUser(roles = {"FRONT_DESK"})
void createAppointment_withInvalidRequest_shouldReturn400() throws Exception {
// Arrange - missing required fields
AppointmentRequestDTO request = new AppointmentRequestDTO();
// Act & Assert
mockMvc.perform(post("/api/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}
}
@ExtendWith(MockitoExtension.class)
class AppointmentServiceTest {
@Mock
private AppointmentRepository appointmentRepository;
@Mock
private AppointmentMapper appointmentMapper;
@InjectMocks
private AppointmentService appointmentService;
@Test
void findById_whenExists_shouldReturnAppointment() {
// Arrange
Appointment appointment = new Appointment();
appointment.setId(1L);
AppointmentResponseDTO dto = new AppointmentResponseDTO(1L, "John", "Alice", "Haircut");
when(appointmentRepository.findById(1L)).thenReturn(Optional.of(appointment));
when(appointmentMapper.toResponseDTO(appointment)).thenReturn(dto);
// Act
AppointmentResponseDTO result = appointmentService.findById(1L);
// Assert
assertThat(result).isNotNull();
assertThat(result.getId()).isEqualTo(1L);
verify(appointmentRepository).findById(1L);
}
@Test
void findById_whenNotExists_shouldThrowException() {
// Arrange
when(appointmentRepository.findById(anyLong())).thenReturn(Optional.empty());
// Act & Assert
assertThatThrownBy(() -> appointmentService.findById(1L))
.isInstanceOf(ResourceNotFoundException.class)
.hasMessageContaining("Appointment not found");
}
}
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestPropertySource(properties = "spring.profiles.active=test")
class AppointmentRepositoryTest {
@Autowired
private AppointmentRepository appointmentRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void findByCustomerId_shouldReturnMatchingAppointments() {
// Arrange
Customer customer = new Customer();
customer.setName("John");
entityManager.persist(customer);
Appointment appointment = new Appointment();
appointment.setCustomer(customer);
entityManager.persist(appointment);
entityManager.flush();
// Act
List<Appointment> results = appointmentRepository.findByCustomerId(customer.getId());
// Assert
assertThat(results).hasSize(1);
assertThat(results.get(0).getCustomer().getName()).isEqualTo("John");
}
}
@ServerSetupExtension
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class AppointmentIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
// Use constants from database defaults
private static final Long EXISTING_CUSTOMER_ID = CustomerDatabaseDefault.JANE_ID;
private static final Long EXISTING_EMPLOYEE_ID = EmployeeDatabaseDefault.ALICE_ID;
private static final Long EXISTING_SERVICE_ID = ServiceTypeDatabaseDefault.HAIRCUT_ID;
private static Long createdAppointmentId;
@Test
@Order(1)
void createAppointment_shouldReturnCreated() throws Exception {
AppointmentRequestDTO request = new AppointmentRequestDTO();
request.setCustomerId(EXISTING_CUSTOMER_ID);
request.setEmployeeId(EXISTING_EMPLOYEE_ID);
request.setServiceTypeId(EXISTING_SERVICE_ID);
request.setAppointmentTime(LocalDateTime.now().plusDays(1));
var result = mockMvc.perform(post("/api/appointments")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").exists())
.andReturn();
String json = result.getResponse().getContentAsString();
createdAppointmentId = objectMapper.readTree(json).get("id").asLong();
}
@Test
@Order(2)
void getAppointment_shouldReturnAppointment() throws Exception {
mockMvc.perform(get("/api/appointments/{id}", createdAppointmentId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(createdAppointmentId));
}
@Test
@Order(3)
void updateAppointment_shouldReturnUpdated() throws Exception {
AppointmentRequestDTO updateRequest = new AppointmentRequestDTO();
updateRequest.setCustomerId(EXISTING_CUSTOMER_ID);
updateRequest.setEmployeeId(EXISTING_EMPLOYEE_ID);
updateRequest.setServiceTypeId(EXISTING_SERVICE_ID);
updateRequest.setAppointmentTime(LocalDateTime.now().plusDays(2));
mockMvc.perform(put("/api/appointments/{id}", createdAppointmentId)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(updateRequest)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(createdAppointmentId));
}
@Test
@Order(4)
void deleteAppointment_shouldReturnNoContent() throws Exception {
mockMvc.perform(delete("/api/appointments/{id}", createdAppointmentId))
.andExpect(status().isNoContent());
}
}
public class AppointmentTestDataBuilder {
private Long id;
private Customer customer;
private Employee employee;
private LocalDateTime appointmentTime;
private String status;
public static AppointmentTestDataBuilder anAppointment() {
return new AppointmentTestDataBuilder()
.withId(1L)
.withAppointmentTime(LocalDateTime.now().plusHours(1))
.withStatus("SCHEDULED");
}
public AppointmentTestDataBuilder withId(Long id) {
this.id = id;
return this;
}
public AppointmentTestDataBuilder withCustomer(Customer customer) {
this.customer = customer;
return this;
}
public Appointment build() {
Appointment appointment = new Appointment();
appointment.setId(id);
appointment.setCustomer(customer);
appointment.setEmployee(employee);
appointment.setAppointmentTime(appointmentTime);
appointment.setStatus(status);
return appointment;
}
public AppointmentRequestDTO buildRequestDTO() {
AppointmentRequestDTO dto = new AppointmentRequestDTO();
dto.setCustomerId(customer != null ? customer.getId() : null);
dto.setEmployeeId(employee != null ? employee.getId() : null);
dto.setAppointmentTime(appointmentTime);
return dto;
}
}
public class AppointmentDatabaseDefault {
public static final Long APPOINTMENT_ID_1 = 100L;
public static final Long APPOINTMENT_ID_2 = 101L;
public static final String INSERT_APPOINTMENT_1 =
"INSERT INTO appointments (id, customer_id, employee_id, appointment_time, status) VALUES " +
"(100, " + CustomerDatabaseDefault.JANE_ID + ", " + EmployeeDatabaseDefault.ALICE_ID + ", " +
"CURRENT_TIMESTAMP + INTERVAL '1 day', 'SCHEDULED')";
public static final String[] ALL_INSERTS = {
INSERT_APPOINTMENT_1
};
}
# Run all tests
.\gradlew.bat check
# Run only unit tests
.\gradlew.bat test
# Run only integration tests
.\gradlew.bat integrationTest
# Run specific test class
.\gradlew.bat test --tests "AppointmentServiceTest"
# Run with coverage
.\gradlew.bat test jacocoTestReport
.\gradlew.bat check