테스트 종류

- 단위 테스트 : 일반적으로 하나의 클래스를 인스턴스화해 해당 클래스의 인터페이스를 통해 기능을 테스트
- 테스트 대상 외에는 모킹을 통해 의존되는 클래스와의 격리
- 통합 테스트 : 연결된 여러 유닛을 인스턴스화한 뒤, 시작점이 되는 클래스의 인터페이스로 데이터를 전송
- 통합 테스트에서도 일부 구간에는 모킹을 수행해야 할 수도 있다.
- 시스템 테스트(UI 테스트) : 모든 객체를 인스턴스화 해 특정 유스케이스가 전체적으로 잘 동작하는지를 테스트
테스트 전략
- 하나의 프로젝트에서 반드시 하나의 테스트 종류만을 사용하는 것은 아님
- 하지만 어떠한 상황에 어떠한 테스트 전략을 취할 것인지에 대한 사전 협의 또는 규칙이 있어야 함
도메인 로직은 단위테스트로
- 도메인 로직은 다른 클래스에 거의 의존하지 않기 때문에 단위테스트로 수행
- 도메인 로직(비즈니스 로직)은 가장 핵심적인 로직이기 때문에 단위적으로 테스트해 로직을 검증
- 도메인 로직의 In-port인 유스케이스까지도 단위테스트로 테스트를 수행하기 수월
- 모든 동작을 검증하는 대신에 중요한 핵심 로직만 골라 집중해서 테스트하는 것이 좋다.
웹 계층은 통합 테스트로
- 웹 어댑터에서는 입력에 대한 유효성 검증 이후 유스케이스에서 사용할 수 있는 포맷으로 매핑하고 결괏값을 JSON으로 파싱
- 웹 컨트롤러는 스프링 프레임워크에 강하게 묶여있기 때문에 프레임워크와 통합된 상태로 테스트하는 것이 효과적
- @WebMvcTest 및 MockMvc, @MockBean등과 같은 스프링 기능을 사용해 주로 구현
@WebMvcTest(controllers = SendMoneyController.class)
class SendMoneyControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private SendMoneyUseCase sendMoneyUseCase;
@Test
void testSendMoney() throws Exception {
mockMvc.perform(post("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}",
41L, 42L, 500)
.header("Content-Type", "application/json"))
.andExpect(status().isOk());
then(sendMoneyUseCase).should()
.sendMoney(eq(new SendMoneyCommand(
new AccountId(41L),
new AccountId(42L),
Money.of(500L))));
}
}
영속성 계층은 통합 테스트로
- 영속성 계층 역시 외부 데이터베이스와 강하게 결합되어 있기 때문에 통합테스트를 진행하는 것이 효과적
- 데이터베이스의 경우 모킹을 하지 않고, 테스트 시에 실제 데이터베이스에 접근해야 에러를 줄일 수 있다.
@DataJpaTest
@Import({AccountPersistenceAdapter.class, AccountMapper.class})
class AccountPersistenceAdapterTest {
@Autowired
private AccountPersistenceAdapter adapterUnderTest;
@Autowired
private ActivityRepository activityRepository;
@Test
@Sql("AccountPersistenceAdapterTest.sql")
void loadsAccount() {
Account account = adapterUnderTest.loadAccount(new AccountId(1L), LocalDateTime.of(2018, 8, 10, 0, 0));
assertThat(account.getActivityWindow().getActivities()).hasSize(2);
assertThat(account.calculateBalance()).isEqualTo(Money.of(500));
}
@Test
void updatesActivities() {
Account account = defaultAccount()
.withBaselineBalance(Money.of(555L))
.withActivityWindow(new ActivityWindow(
defaultActivity()
.withId(null)
.withMoney(Money.of(1L)).build()))
.build();
adapterUnderTest.updateActivities(account);
assertThat(activityRepository.count()).isEqualTo(1);
ActivityJpaEntity savedActivity = activityRepository.findAll().get(0);
assertThat(savedActivity.getAmount()).isEqualTo(1L);
}
}
주요 케이스에 대해 시스템 테스트 수행
- 전체 애플리케이션을 띄우고 API를 통해 모든 계층이 제대로 동작하는지 확인
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SendMoneySystemTest {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private LoadAccountPort loadAccountPort;
@Test
@Sql("SendMoneySystemTest.sql")
void sendMoney() {
Money initialSourceBalance = sourceAccount().calculateBalance();
Money initialTargetBalance = targetAccount().calculateBalance();
ResponseEntity response = whenSendMoney(
sourceAccountId(),
targetAccountId(),
transferredAmount());
then(response.getStatusCode())
.isEqualTo(HttpStatus.OK);
then(sourceAccount().calculateBalance())
.isEqualTo(initialSourceBalance.minus(transferredAmount()));
then(targetAccount().calculateBalance())
.isEqualTo(initialTargetBalance.plus(transferredAmount()));
}
private Account sourceAccount() {
return loadAccount(sourceAccountId());
}
private Account targetAccount() {
return loadAccount(targetAccountId());
}
private Account loadAccount(AccountId accountId) {
return loadAccountPort.loadAccount(
accountId,
LocalDateTime.now());
}
private ResponseEntity whenSendMoney(
AccountId sourceAccountId,
AccountId targetAccountId,
Money amount) {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/json");
HttpEntity<Void> request = new HttpEntity<>(null, headers);
return restTemplate.exchange(
"/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}",
HttpMethod.POST,
request,
Object.class,
sourceAccountId.getValue(),
targetAccountId.getValue(),
amount.getAmount());
}
private Money transferredAmount() {
return Money.of(500L);
}
private Money balanceOf(AccountId accountId) {
Account account = loadAccountPort.loadAccount(accountId, LocalDateTime.now());
return account.calculateBalance();
}
private AccountId sourceAccountId() {
return new AccountId(1L);
}
private AccountId targetAccountId() {
return new AccountId(2L);
}
}
정리
- 도메인 계층을 구현할 때에는 단위 테스트로 커버
- 유스케이스를 구현할 때에는 단위 테스트로 커버
- 어댑터를 구현할 때에는 통합 테스트로 커버
- 사용자가 취할 수 있는 중요 애플리케이션 경로는 시스템 테스트로 커버
💡 테스트가 코드의 구조적 변경에 너무 취약하지 않도록 해야 함 리팩터링할 때마다 테스트 코드도 변경해야 한다면, 테스트는 테스트로서의 가치를 잃음
'Architecture(아키텍처)' 카테고리의 다른 글
| [만들면서 배우는 클린 아키텍처] 09. 애플리케이션 조립하기 (0) | 2024.07.20 |
|---|---|
| [만들면서 배우는 클린 아키텍처] 08. 경계 간 매핑하기 (1) | 2024.07.20 |
| [만들면서 배우는 클린 아키텍처] 06. 영속성 어댑터 구현하기 (0) | 2024.07.20 |
| [만들면서 배우는 클린 아키텍처] 05. 웹 어댑터 구현하기 (0) | 2024.07.20 |
| [만들면서 배우는 클린 아키텍처] 04. 유스케이스 구현하기 (0) | 2024.07.20 |