실행 방법입니다. lib/junit-platform-console-standalone-1.11.4.jar 하나에 JUnit Jupiter·파라미터화·콘솔 런처가 다 들어 있습니다. H2 는 ..\..\lib\h2-2.3.232.jar 입니다.
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// 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;
}
}// 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());
}
}// 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 행이 안 보임
}// 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");
}
}실행하면 아래처럼 트리로 결과가 나옵니다.
├─ 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 로 잘못 바꿔 보면 실패 위치와 값이 정확히 나옵니다.
│ │ ├─ 정상 주문: PAID 로 저장되고 할인 뺀 금액이 결제된다 ✘ Multiple Failures (1 failure)
│ │ │ org.opentest4j.AssertionFailedError: expected: <15001> but was: <15000>
[ 18 tests found ]
[ 17 tests successful ]
[ 1 tests failed ]