한 문단에 한 주제!
하나의 테스트는 하나의 주제를 가져야 한다.
ex) 테스트 코드에서 반복문이나, 분기문 같은 논리 구조 등이 들어가지 않는 형태로 만들어야한다.
만약 테스트에 로직이 들어갔다면, 테스트 케이스가 두 개 이상이다 라는 것을 방증 하기때문에, 별도의 테스트 케이스로 분류해야한다.
완벽하게 제어하기
테스트를 하기 위한 환경을 조성할 때 완벽하게 제어할 수 있어야 한다.
public Order createOrder() {
LocalDateTime currentDateTime = LocalDateTime.now();
LocalTime currentTime = currentDateTime.toLocalTime();
if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요");
}
return new Order(LocalDateTime.now(), beverages);
}
첫 번째 메소드의 LocalDateTime currentDateTime = LocalDateTime.now();
이 변수를 우리가 제어할 수 있는가? 현재 시간의 경우 우리가 제어할 수 없는 환경이다.
그렇기 때문에 두 번째 메소드 처럼 변수를 외부로 빼서 환경을 컨트롤 할 수 있게 한다.
테스트 환경의 독립성을 보장하자
테스트 환경에서 다른 API를 끌어다 사용해서 테스트간 결합도가 생길 수 있다.
@DisplayName("재고가 부족한 상품으로 주문을 생성하려는 경우 예외가 발생한다.")
@Test
void createOrderWithNoStock() {
//재고
Stock stock1 = Stock.create("001", 1);
Stock stock2 = Stock.create("002", 2);
stock1.deductQuantity(1);
stockRepository.saveAll(List.of(stock1,stock2));
OrderCreateServiceRequest request = OrderCreateServiceRequest.builder()
.productNumbers(List.of("001","001","002","003"))
.build();
// when // then
assertThatThrownBy(() -> orderService.createOrder(request,now))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("재고가 부족한 상품이 있습니다.");
}
위 메소드에서 createOrderWithNoStock는 재고를 deductQuantity 통해 차감하고 있다. (다른 API 호출)
만약 stock1.deductQuantity(3); 라면 재고가 1개 뿐이라, 테스트가 실패하게 된다.
하지만 이 테스트 실패는 createOrderWithNoStock의 실패가 아닌 deductQuantity로 인한 실패가 이루어지게 된다. 테스트하고자 하는 부분이 아닌, 다른 부분에서 문제가 생기는 것이다.
이런 문제를 방지하기 위해 순수한 빌더나, 순수한 생성자로만 테스트를 구성하는게 더 나은 테스트 방법이다.
테스트 간 독립성을 보장하자
private static final Stock stock = Stock.create("001",1);
@Test
void isQuantityLessThanEx(){
...생략
stock.isQuantityLessThanEx();
}
@Test
void duductQuantityEx(){
...생략
stock.duductQuantityEx();
}
공유자원인 stock 을 두 메소드에서 모두 사용하고 있다.
테스트에서는 isQuantityLessThanEx → duductQuantityEx 로 이루어져서 통과했다고 가정한다. 하지만 다른 테스트에서는 duductQuantityEx → isQuantityLessThanEx 의 순서로 이루어질 수 있다.
즉, 환경이 달라졌을 때 이 공유 자원이 어떻게 사용될 지 알 수 없으니, 사용하는 것을 지양한다.
한 눈에 들어오는 Test Fixture 구성하기
test Fixture
- Fixture : 고정물, 고정되어 있는 물체
- 테스트를 위해 원하는 상태로 고정시킨 일련의 객체
@BeforeAll
static void BeforeAll() {
//before class
}
@BeforeEach
void setUp(){
//before method
//각 테스트 입장에서 봤을 때 : 아예 몰라도 테스트 내용을 이해하는데 문제가 없는가?
//수정해도 모든 테스트에 영향을 주지 않는가?
}
매번 테스트를 작성 하다 보면, 비슷한 데이터를 테스트에 넣고 있다. 공통으로 생기는 테스트 데이터는 setUp에 넣어서 사용하면 안될까?
안된다. 위의 공유 자원과 동일한 문제가 발생하게 된다.
또한 테스트는 테스트 자체로 문서역할을 하게 된다. 그런데 테스트 할 데이터가 setup처럼 위에 위치하게 되면 given 만 위로 올라가서 결국 정보가 파편화 된다. setUp과 마찬가지로 data.sql을 만들어서 하는것도 동일한 결과를 가져온다.
- 파편화가 일어난다.
- 커질수록 data.sql이 점점커지고, 또 다른 관리포인트가 된다.
private Product createProduct(String productNumber, ProductType type, ProdectSellingStatus sellingStatus,) {
return Product.builder()
.productNumber(productNumber)
.type(type)
.sellingStatus(sellingStatus)
.name("아메리카노")
.price(5000)
.build();
}
createProduct 이러한 given Fixture를 만들어서사용한다면, 모든 값을 받는게 아닌, 테스트에 필요한 데이터들만 받아서 사용하는 걸 추천한다.→ 빌더 역시 매번 파라미터 값이 달라지기 떄문에 매번 새로운 빌드가 생긴다.
아 이런 빌더를 테스트 클래스마다 작성하는게 힘든데, 하나의 클래스에 빌더를 만들고 사용하면 안될까?
** 테스트 클래스마다 만들어 사용하는 것을 추천한다.
- setup 의 경우 테스트 클래스에 지장 없을 때만 사용한다.
- data.sql 미리 setup하는 형태도, test Fixture의 파편화를 만들어내고, 유지보수 포인트가 되기 때문에 권장하지 않음.
- given Fixture룰 구성 할 때, 필요한 파라미터만 명확하게 명시해준다.
- Fixture를 만들기 위한 빌더도 한 곳에 모으는 건 지양하는게 좋다( 복잡해짐)
Test Fixture 클렌징
@AfterEach
void tearDown() {
//테스트가 끝날때마다 삭제
orderProductrRepository.deleteAllInBatch();
productRepository.deleteAllInBatch();
orderRepository.deleteAllInBatch();
stockRepository.deleteAllInBatch();
};
- deleteAllInBatch
- 테이블은 전체 날린다.
- orderProductrRepository를 먼저 지워야하는데, productRepository의 pk가 orderProductrRepository Fk이기 떄문이다.
- 즉, 삭제시 순서를 고려해야함
- deleteAll
- orderRepository 지우고 orderProductrRepository 를 한번에 지워준다.
- 즉, FK 있는 키를 orderProductrRepository 에서 SELECT 해와서 건건이 지운다. 그 후에 orderRepository 지운다.
- 모든 연관관계를 찾아서 SELECT 후에 건건히 DELETE 하기 때문에 성능상 이슈가 있을 수 있음.
- Transaction
- 해당 롤백도 추천한다.
- 롤백 클랜징을 쉽게 해줄 수 있음.
- 하지만 배치 통합 테스트 이런 걸 테스트 할 때는 여러 트랜잭션이 걸리기 때문에 롤백이 어려워 deleteAllInBatch 사용하는 걸 권장한다.
출처 ; https://www.inflearn.com/course/practical-testing-실용적인-테스트-가이드
'TDD' 카테고리의 다른 글
Spring REST Docs (0) | 2024.12.08 |
---|---|
값이나 환경을 바꿔가며 테스트 하고 싶을 때 (0) | 2024.12.08 |
Mockito로 Stubbing 하기 (1) | 2024.12.01 |
응답 예외처리와 책임 분리 (0) | 2024.11.25 |
Presentation Layer의 테스트 (0) | 2024.11.24 |