AI Unified Process Spring Boot layers - service and REST API implementation and tests
95
95%
Does it follow best practices?
Impact
94%
1.04xAverage score across 4 eval scenarios
Passed
No findings from the security scan
The source file UC001RegisterGuestServiceTest.java, wrapped in Markdown so that every skill host installs it.
Use the content of the code block below; a copy written to disk takes the name UC001RegisterGuestServiceTest.java.
package com.example.hotel.guest;
import com.example.hotel.usecase.UseCase;
import jakarta.validation.ConstraintViolationException;
import org.junit.jupiter.api.AfterEach;
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.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.TestPropertySource;
// Testcontainers 1 (Spring Boot 3): org.testcontainers.containers.PostgreSQLContainer, used as PostgreSQLContainer<?>
import org.testcontainers.postgresql.PostgreSQLContainer;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Service tests for UC-001 "Register Guest".
*
* Template for /spring-service-test with the real persistence layer:
* - Class name UC<id><PascalCaseUseCaseName>ServiceTest, every test method annotated @UseCase
* - The service is called directly as a Spring bean — no HTTP, no UI
* - Real database from Testcontainers 2 (Spring Boot 4), schema from the project's Flyway migrations
* - Fixtures come from the Flyway test migration src/test/resources/db/testdata/R__uc001_register_guest.sql:
* insert into guest (id, version, first_name, last_name, email, birth_date, registered_at)
* values (nextval('guest_seq'), 0, 'Alice', 'Example', 'alice@example.com', date '1985-01-01',
* timestamp '2026-01-15 09:00:00');
* A test creates through the service only the guest whose registration it tests
* - A fixed Clock, so rules that depend on "today" have one expected value
* - Each business rule: one allowed and one rejected case; the rejected case asserts the
* exception type and the full message the specification states
* - Not @Transactional: the service's own transaction boundary runs as in production;
* @AfterEach removes only the rows the test created
*/
@SpringBootTest
@Import(UC001RegisterGuestServiceTest.Config.class)
@TestPropertySource(properties = "spring.flyway.locations=classpath:db/migration,classpath:db/testdata")
class UC001RegisterGuestServiceTest {
@TestConfiguration(proxyBeanMethods = false)
static class Config {
@Bean
@ServiceConnection
PostgreSQLContainer postgres() {
return new PostgreSQLContainer("postgres:17-alpine");
}
@Bean
@Primary
Clock fixedClock() {
return Clock.fixed(Instant.parse("2026-03-01T10:00:00Z"), ZoneOffset.UTC);
}
}
private static final LocalDate ARRIVAL = LocalDate.of(2026, 3, 10);
@Autowired
private GuestService guestService;
@Autowired
private GuestRepository guestRepository; // read-only: checks what the service stored
@Autowired
private JdbcTemplate jdbcTemplate;
private final List<Long> createdIds = new ArrayList<>();
@AfterEach
void removeCreatedGuests() {
createdIds.forEach(id -> jdbcTemplate.update("delete from guest where id = ?", id));
createdIds.clear();
}
@Test
@UseCase(id = "UC-001")
void registers_a_guest() {
Guest guest = register("anna@example.com", LocalDate.of(1990, 5, 4));
assertThat(guest.id()).isNotNull();
assertThat(guestService.list(0, 10, new GuestFilter("Muster")))
.extracting(GuestListItem::email).contains("anna@example.com");
}
@Test
@UseCase(id = "UC-001", scenario = "A1: Email Already Registered", businessRules = {"BR-002"})
void rejects_an_email_that_is_already_registered() {
// alice@example.com is registered by the test data migration
assertThatThrownBy(() -> register("alice@example.com", LocalDate.of(1970, 1, 1)))
.isInstanceOf(EmailAlreadyRegisteredException.class)
.hasMessage("The email address alice@example.com is already registered.");
}
@Test
@UseCase(id = "UC-001", businessRules = {"BR-003"})
void accepts_a_guest_who_turns_eighteen_on_the_day_of_arrival() {
Guest guest = register("cara@example.com", ARRIVAL.minusYears(18));
assertThat(guestRepository.findById(guest.id()))
.hasValueSatisfying(stored -> assertThat(stored.birthDate()).isEqualTo(LocalDate.of(2008, 3, 10)));
}
@Test
@UseCase(id = "UC-001", scenario = "A2: Guest Too Young", businessRules = {"BR-003"})
void rejects_a_guest_who_is_under_eighteen_on_the_day_of_arrival() {
assertThatThrownBy(() -> register("dan@example.com", ARRIVAL.minusYears(18).plusDays(1)))
.isInstanceOf(GuestTooYoungException.class)
.hasMessage("Guests must be at least 18 years old on the day of arrival.");
}
@Test
@UseCase(id = "UC-001", scenario = "A3: Invalid Input")
void rejects_a_missing_email() {
assertThatThrownBy(() -> guestService.register(
new RegisterGuest("Eva", "Meier", null, LocalDate.of(1990, 1, 1), ARRIVAL)))
.isInstanceOf(ConstraintViolationException.class);
}
private Guest register(String email, LocalDate birthDate) {
Guest guest = guestService.register(new RegisterGuest("Anna", "Muster", email, birthDate, ARRIVAL));
createdIds.add(guest.id());
return guest;
}
}