@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));
}
ExRateProvider가 제공하는 환율 값으로 계산한 것인가?PaymentService가 SUT라면 WebApiExRateProvider는 협력자다.
    PaymentServiceTest에서 WebApiExRateProvider는 우리가 테스트 하고 싶어하는 대상인가?ExRateProvider는 인터페이스이기 때문에 테스트에서만 구현체를 변경하는 기법을 사용할 수 있다.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));
}
BigDecimal과 isEqualTo 주의
    BigDecimal은 숫자 뿐 아니라 유효 자리수도 따진다.isEqualTo는 내부적윽로 equals 메서드를 사용하는데 BigDecimal의 경우 유효 자리수가 다르면 실제로 값이 같더라도 서로 다른 값이라고 판단해버린다.
        isEqualByComparingTo를 사용하면 된다.
    isComapreTo를 사용ObjectFactory를 구현해보자@Configuration
public class TestObjectFactory {
  @Bean
  public PaymentService paymentService() { return new PaymentService(exRateProvider()); }
  
  @Bean
  public ExRateProvider exRateProvider() { return new ExRateProviderStub(BigDecimal.valueOf(1_1000)); }
}
@ContextConfiguration 어노테이션으로 스프링 구성 정보에 빈을 추가할 수 있고 이를 @Autowired로 받아올 수 있다.@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);
  
    // ...
  }
}
PaymentService의 의존 관계가 변한다고 해도 테스트에서의 DI는 변하지 않아도 된다.Java의 Clock을 사용해서 테스트가 가능하도록 만들어볼 수 있다.
    PaymentService가 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을 스프링 빈으로 만든 뒤 PaymentService에서 주입 받을 필요가 있다.// 보통 이렇게 스프링 구성 정보를 설정하는 책임을 가지는 클래스를 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);
  }
}
Clock을 테스트에서 유용하게 사용할 수 있다.
    ExRateProviderStub처럼 조작한 Clock을 빈으로 띄우면 된다.@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);
  }
}
PaymentService.prepare)Payment)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);
  }
}
Payment 생성을 팩토리 메서드를 통해 의미 있는 로직을 넣어보자.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));  
  }
}
Payment 안에 중요한 로직을 넣어 두었으니 PaymentTest가 필요하다.
    @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);
}