이 레슨의 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요.
OrderService 에 VIP 고객(Set<String> 로 관리)은 5만 원부터 10% 할인을 받는 규칙을 추가하세요. 실패하는 테스트를 먼저 쓰고, 구현을 고쳐 통과시키세요.
// 1) 먼저 실패하는 테스트를 추가한다
@Test
void vip는_5만원부터_할인() {
service = new OrderService(repo, pg, clock, Set.of("kim"));
Order o = service.place("kim", new BigDecimal("50000"));
assertEquals(new BigDecimal("5000"), o.discount()); // 일반은 0원, VIP는 5000원
}
// 2) OrderService 생성자에 vipCustomers 추가, discountFor 를 인스턴스 메서드로 변경
BigDecimal discountFor(String customerId, BigDecimal amount) {
BigDecimal from = vipCustomers.contains(customerId) ? new BigDecimal("50000") : DISCOUNT_FROM;
if (amount.compareTo(from) < 0) return BigDecimal.ZERO;
return amount.multiply(new BigDecimal("0.10")).setScale(0, RoundingMode.DOWN);
}테스트를 구현보다 먼저 쓰면 "무엇이 되어야 하는가" 가 먼저 명확해지고, 구현은 그 뒤를 따라갑니다.
StubGateway 에 처음 N번은 성공, 그 뒤로는 실패하는 옵션을 추가하고, 재시도 없는 OrderService 가 두 번째 주문에서 FAILED 를 남기는지 검증하세요.
static class StubGateway implements PaymentGateway {
final List<BigDecimal> charged = new ArrayList<>();
int failFrom = Integer.MAX_VALUE; // 이 호출 순번부터 실패
public String charge(String customerId, BigDecimal amount) {
charged.add(amount);
if (charged.size() >= failFrom) throw new PaymentException("한도 초과");
return "PAY-" + charged.size();
}
}
@Test
void 두번째_주문부터_실패() {
pg.failFrom = 2;
service.place("kim", new BigDecimal("1000")); // 1번째, 성공
assertThrows(PaymentException.class, () -> service.place("kim", new BigDecimal("1000"))); // 2번째
assertEquals("FAILED", repo.findByCustomer("kim").get(1).status());
}호출 횟수로 성공·실패를 바꿀 수 있게 만들면, 재시도 로직 자체를 테스트할 때도 이 스텁을 그대로 재사용할 수 있습니다.
OrderRepository.findByCustomer 에 상태 파라미터를 추가하고(null 이면 전체), JdbcOrderRepository 와 통합 테스트를 함께 고치세요.
// OrderRepository, JdbcOrderRepository 시그니처 변경
List<Order> findByCustomer(String customerId, String status);
// JdbcOrderRepository: 동적으로 WHERE 절 추가
String sql = "SELECT * FROM orders WHERE customer_id=?" + (status == null ? "" : " AND status=?") + " ORDER BY id";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, customerId);
if (status != null) ps.setString(2, status);
// ... 실행
}
@Test
void findByCustomer_상태필터() {
repo.save(newOrder("kim", "1"));
repo.save(newOrder("kim", "2").withStatus("PAID"));
assertEquals(1, repo.findByCustomer("kim", "PAID").size());
}상태 필터가 없을 때와 있을 때 둘 다 테스트해야, SQL 분기 두 갈래를 모두 실행해 본 것이 됩니다.
주문 목록을 CSV 로 쓰는 기능을 추가하고, @TempDir 로 받은 임시 폴더에 파일을 쓴 뒤 내용을 검증하세요.
static void exportCsv(List<Order> orders, Path file) throws IOException {
var lines = orders.stream().map(o -> o.id() + "," + o.customerId() + "," + o.amount());
Files.write(file, lines.toList());
}
@Test
void csv로_내보내면_한줄에_한주문(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("orders.csv"); // 테스트가 끝나면 JUnit이 자동으로 삭제
exportCsv(List.of(new Order(1L, "kim", new BigDecimal("1000"), BigDecimal.ZERO, "PAID", LocalDateTime.now())), file);
List<String> lines = Files.readAllLines(file);
assertEquals(1, lines.size());
assertTrue(lines.get(0).startsWith("1,kim,"));
}@TempDir 은 테스트마다 새 폴더를 주고 끝나면 정리해서, 파일 경로 충돌이나 뒤처리 코드를 신경 쓸 필요가 없습니다.
InMemoryOrderRepository.save 가 호출될 때마다 인자를 리스트에 쌓게 만들고, 결제 실패 시 save 가 정확히 2번(PENDING, FAILED) 불렸는지 검증하세요. Mockito 버전과 비교하세요.
// 페이크에 호출 기록 추가
static class InMemoryOrderRepository implements OrderRepository {
final List<Order> saveCalls = new ArrayList<>(); // 추가
public Order save(Order o) {
saveCalls.add(o);
Order s = o.id() == null ? o.withId(++seq) : o; rows.put(s.id(), s); return s;
}
// findById, findByCustomer 는 그대로
}
@Test
void 결제실패시_save가_두번_PENDING_다음_FAILED() {
pg.fail = true;
assertThrows(PaymentException.class, () -> service.place("kim", new BigDecimal("1000")));
assertEquals(List.of("PENDING", "FAILED"), repo.saveCalls.stream().map(Order::status).toList());
}
// Mockito 라면 verify(repo, times(2)).save(any()); 로 횟수만 확인, 순서·상태값은 별도 캡처 필요페이크는 호출 기록도 직접 원하는 모양으로 쌓을 수 있어 순서·값을 한 번에 검증하기 쉽고, Mockito 는 횟수 검증은 짧지만 값까지 보려면 ArgumentCaptor 가 추가로 필요합니다.