서론
본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.
Test Fixture 구성하기
중복 발생이 되는 상황들을 한 곳에 묶어 관리하는 개념으로 JUnit에서는, 동일하거나 유사한 개체 집합에 대해 작동하는 두 개 이상의 테스트가 있는 경우 Test Fixture 메서드의 사용을 권장한다.
굳이 메서드를 사용하지 않더라도 픽스처를 구성할 때, 원하는 파라미터만 명확히 명시하여, 테스트 내에서 어떤 파라미터를 통해 확인할 지 명확하게 명시할 수 있어야 한다. 또한 테스트 픽스쳐를 객체의 생성자를 이용해 객체가 생성될 경우를 미리 정의하여 세팅을 따로 해놓았다가 필요한 것만 꺼내서 사용할 수도 있다.
아래는 Fixture 메서드이다.
@BeforeAll
void beforeAll() {
// 테스트 메서드 시작 전 실행
}
@AfterAll
void afterAll() {
// 테스트 메서드 종료 후 실행
}
@BeforeEach
void beforeEach() {
// 각 테스트 메서드 시작 전 실행
}
@AfterEach
void afterEach() {
// 각 테스트 메서드 종료 후 실행
}
@deleteAll, @deleteAllInBatch
강사분을 포함하여 인터넷을 조금만 뒤져봐도, deleteAll 대신 deleteAllInBach를 선호한다고 하는데, 이유를 알아보자
deleteAll은 건 별로 delete 쿼리를 수행한다.
deleteAll의 장점은 매핑되어 있는 테이블을 포함하여 delete를 수행한다는 점이다.
하지만 건 별로 진행되기 때문에 100만 건의 element를 삭제해야 한다면, 100만 건의 delete 쿼리를 수행해야 한다.
/*
* (non-Javadoc)
* @see org.springframework.data.repository.Repository#deleteAll()
*/
@Override
@Transactional
public void deleteAll() {
for (T element : findAll()) {
delete(element);
}
}
deleteAllInBatch는 delete from [table name]의 쿼리를 한 번 수행한다.
/*
* (non-Javadoc)
* @see org.springframework.data.jpa.repository.JpaRepository#deleteInBatch(java.lang.Iterable)
*/
@Override
@Transactional
public void deleteAllInBatch(Iterable<T> entities) {
Assert.notNull(entities, "Entities must not be null!");
if (!entities.iterator().hasNext()) {
return;
}
applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
.executeUpdate();
}
쿼리 성능 측면에서 deleteAllInBatch가 압도적으로 우세할 것 같다. 이렇기 때문에 다들 deleteAllInBatch를 선호하는 것 같다.
@ParameterizedTest
User 클래스가 있고, User 클래스에 대한 테스트를 작성한다고 하자.
public class User {
private String name;
public User(String name) {
validateName(name);
this.name = name;
}
public String getName() {
return name;
}
private void validateName(String name) {
validateNotNull(name);
validateLength(name);
}
private void validateNotNull(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 공백일 수 없습니다.");
}
}
private void validateLength(String name) {
if (name.length > 10) {
throw new IllegalArgumentException("이름은 10자 이하여야 합니다.");
}
}
}
@Test
@DisplayName("이름으로 null 값 예외처리")
void createUserFromNullName() {
assertThatThrownBy(() -> new User(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("공백 이름 예외처리")
void createUserFromBlankName() {
assertThatThrownBy(() -> new User(""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
@DisplayName("10자를 초과하는 이름 예외처리")
void createUserFromGreaterThan10() {
assertThatThrownBy(() -> new User("이 이름은 10자를 넘어가는 이름입니다"))
.isInstanceOf(IllegalArgumentException.class);
}
세 경우 모두 User 객체를 생성하고, 과정에서 IllegalArgumentException을 던지는지 검증하는 로직인데, 같은 로직임에도 테스트가 세 가지 경우나 생겨버렸다. 만약 특수문자 불가능 등 여러 규칙이 더 생성된다면 추가적으로 메서드를 더 작성해주어야 할 것이다.
JUnit에서는 하나의 테스트 케이스인데, 값을 여러 개로 바꿔가며 테스트해보고 싶을 때, 여러 개의 테스트를 한 번에 작성하기 위해 @ParameterizedTest 애너테이션을 제공한다. 값이나 환경 등에 대한 데이터를 변경하여 케이스를 확장하며 테스트를 수행하고 싶을 때 사용한다. 이 때, 파라미터로 넘겨줄 값들을 지정하기 위해 아래의 애너테이션들을 사용한다.
@ValueSource
한 개의 파라미터 입력 시 사용한다.
@ParameterizedTest
@DisplayName("ValueSource를 이용한 User 생성 테스트")
@ValueSource(strings = {"", " "})
void createUserException(String text) {
assertThatThrownBy(() -> new User(text))
.isInstanceOf(IllegalArgumentException.class);
}
@CsvSource
한 파라미터와, 해당 파라미터를 입력했을 때의 출력 값을 입력할 때 사용한다.
int multiplyBy2(int number) {
return number * 2;
}
@ParameterizedTest
@CsvSource(value = {"1,2", "2,4", "3,6"})
void multiplyBy2Test(int input, int expected) {
assertThat(multiplyBy2(input)).isEqualTo(expected);
}
CSV라는 이름에서 알 수 있듯이, input과 expected 값은 콤마로 구분할 수 있다. delimiter값을 정의하여 커스텀 구분자를 사용할 수 있으며, 구분자는 char 값이기 때문에 단일 문자를 넣어주어야 한다. String값을 사용하고 싶다면 delimiterString을 사용한다.
@CsvSource(value = {"1:2", "2:4", "3:6"}, delimiter = ':')
@CsvSource(value = {"1//2", "2//4", "3//6"}, delimiterString = "//")
@NullSource @EmptySource @NullOrEmptySource
Null / 공백 / Null or 공백 상황에 대한 테스트 시 사용한다.
당연한 소리지만, 원시 타입에는 Null이 들어갈 수 없기 때문에 원시 타입을 파라미터로 사용할 경우 @NullSource와 @NullOrEmptySource는 사용이 불가능하다.
@NullSource와 @EmptySource 모두를 사용한 것과 @NullOrEmptySource은 같으며, @ValueSource를 같이 사용할 수 있다.
@ParameterizedTest
@DisplayName("null 값 또는 empty 값으로 User 생성 테스트")
@NullAndEmptySource
@ValueSource(strings = {""," "})
void createUserExceptionFromNullOrEmpty(String text) {
assertThatThrownBy(() -> new User(text))
.isInstanceOf(IllegalArgumentException.class);
}
@EnumSource
Enum값에 대한 테스트 시 사용한다.
value에 Enum 클래스를 넣고, names에 조건으로 지정할 값을 넣어준다.
@ParameterizedTest
@DisplayName("6, 7월이 31일까지 있는지 테스트")
@EnumSource(value = Month.class, names = {"JUNE", "JULY"})
void isJuneAndJuly31(Month month) {
assertThat(month.minLength()).isEqualTo(31);
}
names를 입력했을 때 mode값을 설정할 수 있으며 지원하는 mode값은 다음과 같다.
// name과 일치하는 모든 Enum 값
mode = INCLUDE
// name을 제외한 모든 Enum 값
mode = EXCLUDE
// 조건을 하나라도 만족하는 Enum값
mode = MATCH_ANY
// 조건을 모두 만족하는 Enum값
mode = MATCH_ALL
@MethodSource
파라미터에 메서드를 사용한다.
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForBlankStrings(String input, boolean expected) {
assertThat(input.isBlank()).isEqualTo(expected);
}
private static Stream<Arguments> provideStringsForIsBlank() {
return Stream.of(
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
}
위의 예시는 메서드를 정의하고, 메서드를 @MethodSource를 이용하여 테스트 메서드에 넘겨준다.
이 때, 메서드 이름은 서로 같아야 하며, 메서드 정의는 static으로 해야 한다. 만약 애너테이션에 메서드 이름을 작성하지 않았을 경우 JUnit이 테스트 메서드와 같은 이름의 메서드를 찾아서 파라미터로 제공한다.
References
https://www.baeldung.com/parameterized-tests-junit-5
https://velog.io/@ohzzi/junit5-parameterizedtest
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests
관련 포스팅
테스트 코드를 작성하는 이유 - TestCode (1)
JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)
통합 테스트(Integration Test) - TestCode(6)
@AfterEach, @BeforeEach - TestCode(7)
2023.04 ~ 백엔드 개발자의 기록
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!