[Practical Testing: 실용적인 테스트 가이드]
섹션 8. 더 나은 테스트를 작성하기 위한 구체적 조언
한 문단에는 주제가 너무 다양하거나 여러개이면 안되고 주제가 명확하지 않으면 안된다. ➡ 한 문단에는 단 한가지의 주제만 있어야 한다.
테스트는 문서로서의 기능을 한다.
테스트 코드라는 것을 글쓰기의 관점에서 봤을 때 하나의 테스트가 한 문단이라고 생각을 해보면 하나의 테스트는 하나의 주젬나을 가져야된다라는 원칙이 동일하게 적용될 수 있다.
테스트를 하기 위한 환경을 조성할 때 모든 조건을 완벽하게 제어할 수 있어야 한다.
대부분 given절에서 테스트 행위를 하기 위해 그 이전 준비 과정들을 만든다. 이런 테스트 환경에서 다른 API를 사용하다보면 테스트간 결합도가 생기는 케이스가 발생할 수 있다. 하나의 테스트 내에서의 독립성을 보장하자.
두 개 이상의 테스트에 대한 독립성을 보장하자.
테스트 간에 순서가 없어야 하며 무관해야 한다. 각각 독립적으로 언제 수행되든 항상 같은 결과를 내야 된다. 그러므로 테스트 간 공유 자원을 사용하지 않아야 된다.
@BeforeEach
void setUp() {
// before method
// 각 테스트 입장에서 봤을 때 : 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는가?
// 수정해도 모든 테스트에 영향을 주지 않는가?
// 위의 질문에 만족한 Test Fixture 여기에 존재해도 된다.
}
orderRepository.deleteAll();
orderRepository.deleteAllInBatch();
테이블을 전부 삭제 (Delete From)
순서를 고려해야 한다. Foreign Key(외래키)같은 조건을 가지고 있는 테이블을 먼저 지워야 한다.
연관관계가 있는 테이블을 전체 Select 한 다음 데이터 건당 삭제 처리
모든 연관관계를 확인하고 하나씩 지우기 때문에 성능상 이슈 발생 가능
부작용만 잘 고려하면 롤백 클랜징이 잘 작동된다.
트랜잭션이 여러개 걸리면 트랜잭션 롤백을 사용하기 어렵다. Spring Batch 등을 사용한 배치 통합 테스트 시 여러 트랜잭션 경계가 참여한다.
@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.")
@Test
void containsStockType3() {
// given
ProductType givenType1 = ProductType.HANDMADE;
ProductType givenType2 = ProductType.BOTTLE;
ProductType givenType3 = ProductType.BAKERY;
// when
boolean result1 = ProductType.containsStockType(givenType1);
boolean result2 = ProductType.containsStockType(givenType2);
boolean result3 = ProductType.containsStockType(givenType3);
// then
assertThat(result1).isFalse();
assertThat(result2).isTrue();
assertThat(result3).isTrue();
}
ProductType
에 대해서 체크 매소드를 확인해보고 싶다.
핸드메이드일 때는 재고와 관련된 타입이 아니기 때문에 false
가 나올 것이고 병음료나 베이커리 같은 경우에는 true
가 나올 것이다.
하나의 케이스에서 값만 변경하여 모든 상황을 체크하고 싶은 상황에서 활용할 수 있는 게 @ParameterizedTest
이다.
@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.")
@CsvSource({"HANDMADE,false","BOTTLE,true","BAKERY,true"})
@ParameterizedTest
void containsStockType4(ProductType productType, boolean expected) {
// when
boolean result = ProductType.containsStockType(productType);
// then
assertThat(result).isEqualTo(expected);
}
Csv로 데이터값과 결과값을 넣어줄 수 있다.
private static Stream<Arguments> provideProductTypesForCheckingStockType() {
return Stream.of(
Arguments.of(ProductType.HANDMADE, false),
Arguments.of(ProductType.BOTTLE, true),
Arguments.of(ProductType.BAKERY, true)
);
}
@DisplayName("상품 타입이 재고 관련 타입인지를 체크한다.")
@MethodSource("provideProductTypesForCheckingStockType")
@ParameterizedTest
void containsStockType5(ProductType productType, boolean expected) {
// when
boolean result = ProductType.containsStockType(productType);
// then
assertThat(result).isEqualTo(expected);
}
혹은 매서드를 생성하여 사용할 수 있다.
📑 @ParameterizedTest 공식 문서
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests
여러 테스트들이 하나의 공유변수를 사용하여 테스틑 하는 것을 지양할 필요가 있다. 테스트 간에 순서가 생기고 서로 강결합 형태가 되기 때문에 독립성이 보장되지 않기 때문이다.
그럼에도 불구하고 어떤 하나의 환경을 설정해놓고 이 환경에 변화를 주면서 사용자 시나리오를 테스트하고 싶다면 @DynamicTest
를 사용하는 것이 좋다.
@DisplayName("")
@TestFactory
Collection<DynamicTest> dynamicTest() {
return List.of(
DynamicTest.dynamicTest("", () -> {
}),
DynamicTest.dynamicTest("", () -> {
})
);
}
테스트는 사람이 수동으로 검증하는 것대신 기계의 도움을 받아서 수시로 피드백을 받고 프로덕션 코드를 발전시켜 나갈 수 있는 도구이다.
이 테스트 자체를 자주 수행하려면 테스트가 수행되는 시간 같은 것들이 전부 비용이기 때문에 관리가 필요하다.
@ActiveProfiles("test")
@SpringBootTest
class OrderServiceTest {
...
class OrderServiceTest extends IntegrationTestSupport {
서버의 공통적인 환경을 하나의 추상 클래스로 만들어서 상속 및 관리하면 서버가 뜨는 횟수가 줄어든다.
A. 할 필요가 없다!
어차피 하려고 해도 안된다!
private 메서드를 테스트하고 싶은 시점에 "객체를 분리할 시점인가?" 라는 질문을 던져봐야 한다.
public 메서드를 테스트 하면서 private 메서드도 함께 검증이 된다. public 메서드를 테스트하면서 private 메서드도 함께 검증이 되는 게 맞는가?라는 의문이 들면 객체를 분리할 시점인지 고민해보면 된다. public 메서드의 책임과 private 메서드의 책임을 별개로 보고 다른 클래스로 분리해야 한다.
A. 만들어도 된다. 하지만 "보수적으로" 접근하기!
테스트에서 꼭 사용해야만 할 것 같은 메소드들은 만들어도 된다.
테스트에서만 사용되는 메소드를 막 만들어내는 것은 지양해야 한다. 무엇을 테스트하고 있는지를 명확히 인지를 해야 한다.
📑 출처