TIL

섹션 4. 테스트

PaymentService 테스트

@Test
void prepare() throws IOException {
  PaymentService paymentService = 
      new PaymentService(new WebApiExRateProvider());
  
  Payment payment = paymentService.prepare(1L, "USD", BigDecimal.TEN);
  
  // 환율 정보 가져오기
  assertThat(payment.getExRate()).isNotNull();
  
  // 원화 환산 금액 계산
  assertThat(payment.getConvertedAmount())
    .isEqualTo(paymentExRate().multiply(payment.getForeignCurrencyAmount()));
    
  // 원화 환산 금액의 유효 계산 시간
  assertThat(payment.getValidUntil()).isAfter(LocalDateTime.now());
  assertThat(payment.getValidUntil()).isBefore(LocalDateTime.now().plusMinutes(30));
}

테스트 구성 요소

SUT

테스트와 DI

public class ExRateProviderStub implements ExRateProvider {
  private BigDecinal exRate; // 미리 정해둔 값을 리턴하도록
  
  public ExRateProviderStub(BigDecimal exRate) {
    this.exRate = exRate;
  }
  
  @Override
  public BigDecimal getExRate(String currency) throw IOException {
    return exRate;
  }
}
@Test
void prepare() throws IOException {
  PaymentService paymentService = 
      new PaymentService(new ExRateProviderStub(BigDecimal.valueOf(500)));
  
  Payment payment = paymentService.prepare(1L, "USD", BigDecimal.TEN);
  
  // 환율 정보 가져오기
  assertThat(payment.getExRate()).isEqualTo(BigDecimal.valueOf(500));;
  
  // 원화 환산 금액 계산
  assertThat(payment.getConvertedAmount())
    .isEqualTo(BigDecimal.valueOf(5000));
    
  // 원화 환산 금액의 유효 계산 시간
  assertThat(payment.getValidUntil()).isAfter(LocalDateTime.now());
  assertThat(payment.getValidUntil()).isBefore(LocalDateTime.now().plusMinutes(30));
}

스프링 DI를 이용하는 테스트

@Configuration
public class TestObjectFactory {
  @Bean
  public PaymentService paymentService() { return new PaymentService(exRateProvider()); }
  
  @Bean
  public ExRateProvider exRateProvider() { return new ExRateProviderStub(BigDecimal.valueOf(1_1000)); }
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestObjectFactory.class)
class PaymentServiceSpringTest {
  @Autowired
  private PaymentService paymentService;

  @Test
  void prepare() throws IOException {
    Payment payment = paymentService.prepare(1L, "USD", BigDecimal.TEN);
  
    // ...
  }
}

학습 테스트

Clock

Clock 학습 테스트 예제

- test
  - java
    - tobyspring.hellospring
      - learningtest
        - ClockTest.java
      - payment
class ClockTest {
  @Test
  void clock을_이용해서_LocalDateTime을_호출() {
    Clock clock = Clock.systemDefaultZone();
    
    LocalDateTime dt1 = LocalDateTime.now(clock);
    LocalDateTime dt2 = LocalDateTime.now(clock);
    
    assertThat(dt2).isAfter(dt1);
  }
  
  @Test
  void clock을_사용해_원하는_시간을_지정할_수_있다() {
    Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
    
    LocalDateTime dt1 = LocalDateTime.now(clock);
    LocalDateTime dt2 = LocalDateTime.now(clock);
    
    assertThat(dt2).isEqualTo(dt1);
  }
}

Clock을 이용한 시간 테스트

// 보통 이렇게 스프링 구성 정보를 설정하는 책임을 가지는 클래스를 XXXConfig 혹은 XXXConfiguration이라 부른다.
// ObjectFactory -> PaymentConfig라고 이름을 변경
@Configuration
public class PaymentConfig {
  @Bean
  public PaymentService paymentService() {
    return new PaymentService(cachedExRateProvider(), clock());
  }
  
  // ...
  
  @Bean
  public Clock clock() { return Clock.systemDefaultZone(); }
}
public class PaymentService {
  private final ExRateProvider exRateProvider;
  private final Clock clock; // 의존성 주입
  
  public PaymentService(ExRateProvider exRateProvider, Clock clock) {
    this.exRateProvider = exRateProvider;
    this.clock = clock;
  }

  public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
    BigDecimal exRate = exRateProvider.getExRate(currency);
    BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exrate);
    LocalDateTime validUntil = LocalDateTime.now(clock).plusMinutes(30);
    
    return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
  }
}
@Configuration
public class TestPaymentConfig {
  @Bean
  public PaymentService paymentService() { return new PaymentService(exRateProvider()); }
  
  @Bean
  public ExRateProvider exRateProvider() { return new ExRateProviderStub(BigDecimal.valueOf(1_1000)); }
  
  // 항상 고정된 시간을 가지는 시계를 빈으로 구성
  @Bean
  public Clock clock() { return Clock.fixed(Instant.now(), ZoneId.systemDefault());
}
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestObjectFactory.class)
class PaymentServiceSpringTest {
  @Autowired
  private PaymentService paymentService;
  @Autowired
  private Clock clock; // 고정된 시간을 가지는 Clock

  // ...

  @Test
  void validUntil() throws IOException {
    Payment payment = paymentService.prepare(1L, "USD", BigDecimal.TEN);
  
    // valid until이 prepare() 30분 뒤로 설정됐는가?
    LocalDateTime now = LocalDateTime.now(this.clock);
    LocalDateTime expectedValidIntil = now.plutMinutes(30);
    
    assertThat(payment.getValidUntil()).isEqualTo(expectedValidUntil);
  }
}

도메인 오브젝트 테스트 (테스트의 꽃)

도메인 모델 아키텍처 패턴

public class PaymentService {
  // ...
  public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
    BigDecimal exRate = exRateProvider.getExRate(currency);
    BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exrate);
    LocalDateTime validUntil = LocalDateTime.now(clock).plusMinutes(30);
    
    return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
  }
}
public class Payment {
  // ...
  
  public static Payment createPrepared(Long orderId, String currency, BigDecimal foreignCurrencyAmount, BigDecimal exRate,
                                       LocalDateTime now) {
    BigDecimal convertedAmount = foreignCurrencyAmount.multiply(exrate);
    LocalDateTime validUntil = now.plusMinutes(30);
    
    return new Payment(orderId, currency, foreignCurrencyAmount, exRate, convertedAmount, validUntil);
  }
}
public class PaymentService {
  // ...
  public Payment prepare(Long orderId, String currency, BigDecimal foreignCurrencyAmount) throws IOException {
    BigDecimal exRate = exRateProvider.getExRate(currency);
  
    return Payment.createPrepared(orderId, currency, foreignCurrencyAmount, exRate, LocalDateTime.now(clock));  
  }
}
@Test
void createPrepared() {
  Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
  
  Payment payment = Payment.createPrepared(
    1L, "USD", BigDecimal.TEN, BigDecimal.valueOf(1_000), LocalDateTime.now(clock)
  );
  
  assertThat(payment.getConvertedAmount()).isEqualByComparingTo(BigDecimal.valueOf(10_000));
  assertThat(payment.getValidUntil()).isEqualTo(expectedValidUntil);
}