공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 12 / 22

테스트 실무

JUnit 5 단위·통합 테스트, 테스트 더블, 시계 제어, H2 롤백
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

이 레슨의 파일에 직접 코드를 덧붙이는 문제 5개입니다. 정답을 보기 전에 먼저 힌트만 보고 스스로 작성해 보세요.

문제 1. VIP 고객은 5만 원부터 할인

OrderService 에 VIP 고객(Set<String> 로 관리)은 5만 원부터 10% 할인을 받는 규칙을 추가하세요. 실패하는 테스트를 먼저 쓰고, 구현을 고쳐 통과시키세요.

java
// 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);
}

테스트를 구현보다 먼저 쓰면 "무엇이 되어야 하는가" 가 먼저 명확해지고, 구현은 그 뒤를 따라갑니다.

문제 2. StubGateway 에 "2번째 호출부터 실패" 추가

StubGateway 에 처음 N번은 성공, 그 뒤로는 실패하는 옵션을 추가하고, 재시도 없는 OrderService 가 두 번째 주문에서 FAILED 를 남기는지 검증하세요.

java
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());
}

호출 횟수로 성공·실패를 바꿀 수 있게 만들면, 재시도 로직 자체를 테스트할 때도 이 스텁을 그대로 재사용할 수 있습니다.

문제 3. findByCustomer 에 상태 필터 추가

OrderRepository.findByCustomer 에 상태 파라미터를 추가하고(null 이면 전체), JdbcOrderRepository 와 통합 테스트를 함께 고치세요.

java
// 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 분기 두 갈래를 모두 실행해 본 것이 됩니다.

문제 4. `@TempDir` 로 CSV 내보내기 테스트

주문 목록을 CSV 로 쓰는 기능을 추가하고, @TempDir 로 받은 임시 폴더에 파일을 쓴 뒤 내용을 검증하세요.

java
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 은 테스트마다 새 폴더를 주고 끝나면 정리해서, 파일 경로 충돌이나 뒤처리 코드를 신경 쓸 필요가 없습니다.

문제 5. 페이크에 호출 기록을 추가해 저장 횟수 검증

InMemoryOrderRepository.save 가 호출될 때마다 인자를 리스트에 쌓게 만들고, 결제 실패 시 save 가 정확히 2번(PENDING, FAILED) 불렸는지 검증하세요. Mockito 버전과 비교하세요.

java
// 페이크에 호출 기록 추가
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 가 추가로 필요합니다.

연습 문제
    이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리