@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);
}