공공부하자개발 · 영어 학습 노트
자바
실무 확장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정리‹ 이전다음 ›

3. 코드 예제

실행 방법입니다. lib/junit-platform-console-standalone-1.11.4.jar 하나에 JUnit Jupiter·파라미터화·콘솔 런처가 다 들어 있습니다. H2 는 ..\..\lib\h2-2.3.232.jar 입니다.

powershell
cd java-src\extension\12_testing
C:\project\jdk-21.0.8\bin\javac -encoding UTF-8 -cp "lib\*;..\..\lib\*" *.java
C:\project\jdk-21.0.8\bin\java -Dstdout.encoding=UTF-8 -cp "lib\*;..\..\lib\*;." Main

예제 1: OrderService — 생성자로 세 의존을 받는다

java
// OrderService.place
public Order place(String customerId, BigDecimal amount) {
    if (amount == null || amount.signum() <= 0) throw new IllegalArgumentException("금액은 0보다 커야 합니다");
    LocalDateTime now = LocalDateTime.now(clock);              // 진짜 now() 대신 주입받은 clock
    LocalTime t = now.toLocalTime();
    if (t.isBefore(OPEN) || !t.isBefore(CLOSE)) throw new IllegalStateException("영업시간(09:00~22:00) 외 주문 불가");

    Order order = repo.save(new Order(null, customerId, amount, discountFor(amount), "PENDING", now));
    try {
        pg.charge(customerId, order.payable());
        return repo.save(order.withStatus("PAID"));
    } catch (PaymentGateway.PaymentException e) {
        repo.save(order.withStatus("FAILED"));                 // 실패 이력도 남긴다
        throw e;
    }
}

예제 2: 페이크·스텁과 단위 테스트

java
// OrderServiceTest
static class InMemoryOrderRepository implements OrderRepository {
    final Map<Long, Order> rows = new HashMap<>();
    long seq = 0;
    public Order save(Order o) { Order s = o.id() == null ? o.withId(++seq) : o; rows.put(s.id(), s); return s; }
    public Optional<Order> findById(long id) { return Optional.ofNullable(rows.get(id)); }
    public List<Order> findByCustomer(String c) { return rows.values().stream().filter(o -> o.customerId().equals(c)).toList(); }
}

static class StubGateway implements PaymentGateway {
    final List<BigDecimal> charged = new ArrayList<>();
    boolean fail;
    public String charge(String customerId, BigDecimal amount) {
        charged.add(amount);
        if (fail) throw new PaymentException("한도 초과");
        return "PAY-" + charged.size();
    }
}

@BeforeEach
void setUp() {                                          // 테스트마다 새로 만들어 서로 오염되지 않게
    repo = new InMemoryOrderRepository();
    pg = new StubGateway();
    clock = Clock.fixed(BUSINESS_HOUR, SEOUL);
    service = new OrderService(repo, pg, clock);
}

@Test
void place_ok() {
    Order o = service.place("kim", new BigDecimal("150000"));
    assertAll(
            () -> assertEquals("PAID", o.status()),
            () -> assertEquals(new BigDecimal("15000"), o.discount()),
            () -> assertEquals(List.of(new BigDecimal("135000")), pg.charged));
}

@ParameterizedTest(name = "{0}원 → 할인 {1}원")
@CsvSource({"99999, 0", "100000, 10000", "100001, 10000", "250005, 25000"})
void discount_boundary(String amount, String expected) {
    assertEquals(new BigDecimal(expected), OrderService.discountFor(new BigDecimal(amount)));
}

@Nested
@DisplayName("취소")
class Cancel {
    @Test
    void within24h_ok() {
        Order o = service.place("kim", new BigDecimal("1000"));
        service = new OrderService(repo, pg, Clock.offset(clock, Duration.ofHours(23)));   // 시계를 23시간 돌림
        assertEquals("CANCELED", service.cancel(o.id()).status());
    }
}

예제 3: H2 통합 테스트 — 트랜잭션 롤백으로 격리

java
// JdbcOrderRepositoryTest
@BeforeAll
static void openDb() throws SQLException {
    conn = DriverManager.getConnection("jdbc:h2:mem:orders;DB_CLOSE_DELAY=-1", "sa", "");
    try (var st = conn.createStatement()) { st.execute(JdbcOrderRepository.DDL); }
}

@BeforeEach
void begin() throws SQLException {
    conn.setAutoCommit(false);                          // 트랜잭션 시작
    repo = new JdbcOrderRepository(conn);
}

@AfterEach
void rollback() throws SQLException { conn.rollback(); }   // 테스트가 남긴 행을 전부 되돌린다

@Test
void save_then_find() {
    Order saved = repo.save(newOrder("kim", "1000"));
    assertNotNull(saved.id());
    Order found = repo.findById(saved.id()).orElseThrow();
    assertEquals(0, found.amount().compareTo(new BigDecimal("1000")));  // DECIMAL 스케일 차이 무시
}

@Test
void isolated_from_other_tests() {
    assertTrue(repo.findByCustomer("kim").isEmpty());       // 앞 테스트에서 넣은 kim 행이 안 보임
}

예제 4: 콘솔 런처 — Gradle 없이 실행

java
// Main.java
public class Main {
    public static void main(String[] args) {
        org.junit.platform.console.ConsoleLauncher.main(
                "execute", "--class-path", ".",
                "--scan-class-path",                       // 현재 폴더에서 @Test 가 있는 클래스를 전부 찾는다
                "--details", "tree",
                "--disable-banner", "--disable-ansi-colors");
    }
}

실행하면 아래처럼 트리로 결과가 나옵니다.

text
├─ JUnit Jupiter ✔
│  ├─ OrderServiceTest ✔
│  │  ├─ 결제 실패: FAILED 로 남기고 예외를 그대로 올린다 ✔
│  │  ├─ 할인 경계값: 10만 원부터 10%, 원 단위 절사 ✔
│  │  ├─ 정상 주문: PAID 로 저장되고 할인 뺀 금액이 결제된다 ✔
│  │  └─ 취소 ✔
│  └─ JdbcOrderRepositoryTest ✔
│     ├─ save 는 id 를 채번해 돌려주고 findById 로 같은 행을 읽는다 ✔
│     └─ 이전 테스트의 행은 롤백돼 보이지 않는다 ✔

Test run finished after 3569 ms
[        18 tests found           ]
[        18 tests successful      ]
[         0 tests failed          ]

"Multiple Failures" 는 assertAll 안의 검증 중 하나 이상이 깨졌다는 뜻이고, 실패한 검증마다 기대값과 실제값을 따로 보여 줍니다. 여러 필드를 한 번에 검증해도 원인 파악이 늦어지지 않는 이유입니다.

기대값을 15001 로 잘못 바꿔 보면 실패 위치와 값이 정확히 나옵니다.

text
│  │  ├─ 정상 주문: PAID 로 저장되고 할인 뺀 금액이 결제된다 ✘ Multiple Failures (1 failure)
│  │  │        	org.opentest4j.AssertionFailedError: expected: <15001> but was: <15000>
[        18 tests found           ]
[        17 tests successful      ]
[         1 tests failed          ]

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다. 외부 jar 를 쓰는 레슨은 java-src/lib 를 클래스패스에 넣습니다.

cd java-src\extension\12_testing
javac -encoding UTF-8 *.java && java Main

:: 외부 jar 가 필요한 레슨
javac -encoding UTF-8 -cp "..\..\lib\*;." *.java && java -cp "..\..\lib\*;." Main
코드 예제
  • 예제 1: OrderService — 생성자로 세 의존을 받는다
  • 예제 2: 페이크·스텁과 단위 테스트
  • 예제 3: H2 통합 테스트 — 트랜잭션 롤백으로 격리
  • 예제 4: 콘솔 런처 — Gradle 없이 실행
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제