더 좋은 테스트 코드 작성하기 2 - Test Fixture 구성하기 / TestCode(10)

Tech/기타 2023. 7. 13. 22:40
728x90
728x90

서론

본 포스팅은 아래의 인강을 듣고, 추가 공부가 필요한 내용들을 포함하여 정리한 포스팅입니다.

 

Practical Testing: 실용적인 테스트 가이드 - 인프런 | 강의

이 강의를 통해 실무에서 개발하는 방식 그대로, 깔끔하고 명료한 테스트 코드를 작성할 수 있게 됩니다. 테스트 코드가 왜 필요한지, 좋은 테스트 코드란 무엇인지 궁금하신 모든 분을 위한 강

www.inflearn.com

 


 

 

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)

단위 테스트 - TestCode (2)

JUnit 5를 사용한 Java 단위 테스트 - TestCode (3)

TDD - TestCode (4)

BDD - TestCode(5)

통합 테스트(Integration Test) - TestCode(6)

@AfterEach, @BeforeEach - TestCode(7)

Mock / Test Double - TestCode (8)

더 좋은 테스트 작성하기(1) - TestCode(9)

 

 

728x90
300x250
mag1c

mag1c

2년차 주니어 개발자.

방명록