티스토리 뷰
대부분의 테스트는 특정 메소드에 특정 인자를 전달하였을 때 기대한 값이 맞는지 확인하는 방식으로 이루어진다. 이때 전달하는 인자로 일반적인 값뿐만 아니라 edge case 혹은 corner case 등까지 포함해야 제대로 된 테스트라 할 수 있다.
아래와 같이 A
클래스에서 인자로 전달된 값을 바로 반환하는 메소드를 테스트한다고 가정해보자. 메소드가 제대로 동작하는지 확인하기 위해서는 하나의 값 뿐만 아니라 다양한 케이스에 해당하는 값들을 인자로 넘겨 테스트를 수행해야 한다. 그런데 아래와 같이 get
테스트 대상 메소드에 전달하는 인자 별로 테스트 메소드를 분리하게 되면, 중복 코드가 많아지고 테스트 클래스의 크기도 커져 무엇을 테스트 하고 있는지 한눈에 알아보기 어려워진다.
따라서 JUnit에서는 테스트 메소드 1개만으로도 다양한 케이스를 테스트하고 각 케이스의 독립적인 수행 또한 보장하는 Parameterized Tests 라는 기능을 제공한다. 이번 글에서는 이 Parameterized Tests의 동작 방식과 적용 방법에 대해 간단히 알아보고자 한다.
class UnitTest {
static class A {
int get(int a) {
return a;
}
}
@Test
void test1() {
A a = new A();
assertThat(a.get(1)).isEqualTo(1);
}
@Test
void test2() {
A a = new A();
assertThat(a.get(987654321)).isEqualTo(987654321);
}
@Test
void test3() {
A a = new A();
assertThat(a.get(-987654321)).isEqualTo(-987654321);
}
}
@ParameterizedTest
먼저 테스트 메소드에 Parameterized Tests를 적용하기 위해서는 @ParameterizedTest
어노테이션을 해당 테스트 메소드 위에 선언해주어야 한다. (이때 @ParameterizedTest
가 @Test
어노테이션의 역할을 대신하기 때문에 @Test
어노테이션은 선언해줄 필요가 없다.) 이후 해당 테스트 메소드에게 전달할 arguments를 제공하는 1개 이상의 source를 선언해주어야 한다. 이 또한 어노테이션으로 지정 가능하며 @ValueSource
, @NullSource
, @EmptySource
, @EnumSource
, @MethodSource
등이 존재한다. 이제 하나씩 자세히 살펴보자.
@ParameterizedTest
@ValueSource(ints = {1, 987654321, -987654321})
void test(int num) {
A a = new A();
assertThat(a.get(num)).isEqualTo(num);
}
@ValueSource
@ValueSource
어노테이션을 사용하면 특정 타입의 배열을 arguments로 지정할 수 있다. 따라서 배열의 각 요소가 테스트 메소드의 파라미터로 하나씩 전달되면서 이를 기반으로 한 테스트가 독립적으로 수행된다. 실제 아래 코드를 실행시켜 보면 {"Java", "Spring", "Spring Boot"}
의 각 요소 마다의 독립적인 테스트를 보장하기 위해 UnitTest 클래스의 생성자가 매번 호출되고 이후 각 요소를 파라미터로 전달받은 test 테스트 메소드가 호출된다.
참고로 @ValueSource
에서 arguments로 지정 가능한 타입에는 short
, byte
, int
, long
, float
, double
, char
, boolean
, java.lang.String
, java.lang.Class
가 있다.
class UnitTest {
public UnitTest() {
System.out.println("UnitTest 생성자");
}
@ParameterizedTest
@ValueSource(strings = {"Java", "Spring", "SpringBoot"})
void test(String str) {
assertThat(str).isNotEmpty();
System.out.println("str = " + str);
}
}
.
@NullSource, @EmptySource
잘못된 입력값 또한 테스트하기 위해 null, emtpy 값을 전달해줘야 하는 경우가 있다. 물론 @ValueSource
를 사용해도 되지만 특별한 입력값인 만큼 별도의 어노테이션이 존재한다. @NullSource
, @EmptySource
어노테이션은 각각 하나의 null 값, 하나의 empty 값을 테스트 메소드의 파라미터로 전달한다.
당연히 테스트 메소드의 파라미터가 primitive type인 경우에는 @NullSource
어노테이션을 사용할 수 없으며, 마찬가지로 String
, List
, Set
, Map
, primitive Arrays
, Object Arrays
타입의 파라미터인 경우에만 @EmptySource
어노테이션을 사용할 수 있다. 참고로 만약 이 두 어노테이션이 모두 선언되어 있다면 단순히 코드의 양을 줄이기 위해 @NullAndEmptySource
어노테이션으로 대체할 수 있다.
@ParameterizedTest
@NullSource
@ValueSource(strings = {""})
void testNullSource(String str) {
assertThat(str).isNullOrEmpty();
}
@ParameterizedTest
@EmptySource
void testEmptySource(List<Integer> list) {
assertThat(list).hasSize(0)
.isEmpty();
}
@EnumSource
테스트 메소드의 파라미터로 Enum 타입 또한 전달해줄 수 있다. 이를 위해 @EnumSource
어노테이션을 사용하는데 지정 가능한 각 속성의 의미는 다음과 같다.
value | 파라미터로 전달하는 Enum의 클래스 타입 지정 (생략될 경우 해당 테스트 메소드의 첫번째 파라미터의 타입이 사용됨) |
names | 어떤 Enum constants를 파라미터로 전달할 것인지 이름이나 정규 표현식을 지정 (생략될 경우 해당 Enum 내에 선언된 모든 constants가 전달됨) |
mode | names 속성에서 지정한 constants만을 포함할 것인지, 제외할 것인지 등을 지정 |
아래 코드를 보며 조금 더 자세히 살펴보자. 각 설정 별 의미 또한 표로 정리해보았다.
public enum Lang {
JAVA, C, PYTHON, KOTLIN //Enum constants
}
@ParameterizedTest
@EnumSource(names = {"JAVA", "C"})
//@EnumSource(value = Lang.class, mode = EXCLUDE, names = {"KOTLIN"})
//@EnumSource(value = Lang.class, mode = MATCH_ALL, names = {".{6}", ".*N$"})
void testValueSource(Lang lang) {
assertThat(lang).isNotNull();
}
@EnumSource(names = {"JAVA", "C"}) |
테스트 메소드의 첫번째 파라미터인 Lang 타입의 Enum에서 JAVA, C 라는 constants만을 파라미터로 전달 |
@EnumSource(value = Lang.class, mode = EXCLUDE, names = {"KOTLIN"}) |
Lang 타입의 Enum에서 KOTLIN 제외한 모든 constants를 파라미터로 전달 (즉 JAVA, C, PYTHON constants 전달) |
@EnumSource(value = Lang.class, mode = MATCH_ALL, names = {".{6}", ".*N$"}) |
Lang 타입의 Enum에서 글자수가 6이면서 N으로 끝나는 constants만을 파라미터로 전달 (즉 PYTHON, KOTLIN constants 전달), MATCH_ALL mode는 names에서 지정한 속성값을 모두 만족하는 constants만을 선택 |
@MethodSource
예를 들어 테스트 메소드의 arguments로 1~100까지의 정수를 넘겨주고자 할 때 @ValueSource
어노테이션을 사용하면 1~100까지의 숫자를 직접 하드코딩해야 한다. 따라서 이 경우에는 @MethodSource
어노테이션을 사용하여 해당 arguments를 대신 생성해주는 팩토리 메소드의 이름을 지정해주는 것이 더욱 간편하다. 이때 팩토리 메소드는 static이면서 Stream
타입의 객체를 반환해야 한다.
@ParameterizedTest
@ValueSource(ints = {1, 2, ... ,99, 100})
void testWithValueSource(int num) {
assertThat(num).isLessThanOrEqualTo(100);
}
@ParameterizedTest
@MethodSource("intArgsProvider")
void testWithMethodSource(int num) {
assertThat(num).isLessThanOrEqualTo(100);
}
//팩토리 메소드
static Stream<Integer> intArgsProvider() {
return Stream.iterate(1, i -> i <= 100, i -> i + 1);
}
또한 별도로 정의한 클래스의 객체를 arguments로 전달하기 위해서도 @MethodSource
어노테이션을 사용해야 한다. 아래 코드와 같이 팩토리 메소드가 Stream<(클래스)>
타입의 객체를 반환하면 @MethodSource
가 선언된 테스트 메소드에서는 해당 클래스의 객체를 파라미터로 전달받아 테스트를 수행할 수 있다.
@ParameterizedTest
@MethodSource("objArgsProvider")
void testWithMethodSource(A a) {
assertThat(a).isInstanceOf(A.class)
.isNotNull();
}
//팩토리 메소드
static Stream<A> objArgsProvider() {
return Stream.of(new A(1), new A(2));
}
그렇다면 테스트 메소드에 여러 타입의 파라미터를 동시에 전달하고 싶은 경우에는 어떻게 해야 할까? 일단 앞서 소개한 @ValueSource
, @EnumSource
등은 동시에 여러 개 선언될 수 없어 다수의 파라미터를 제공줄 수 없다. 이때에도 @MethodSource
를 사용하면 되는데, Stream<Arguments>
객체를 반환하는 팩토리 메소드를 정의하고 해당 메소드의 이름을 @MethodSource
어노테이션에서 지정해주면 된다. 아래 코드의 경우 팩토리 메소드로부터 String
, int
, List<Integer>
타입의 파라미터를 동시에 전달받고 있다. 참고로 Arguments
는 테스트 메소드에게 여러 타입의 파라미터를 동시에 전달하기 위한 JUnit의 인터페이스이다.
이외에도 다양한 방식으로 팩토리 메소드를 사용하여 테스트 메소드에 전달되는 arguments를 지정할 수 있는데 더 자세한 내용을 다음을 참고하길 바란다.
@ParameterizedTest
@MethodSource("multiArgsProvider")
void testWithMethodSource(String str, int num, List<Integer> list) {
assertThat(str).hasSize(5);
assertThat(num).isGreaterThan(5);
assertThat(list).hasSize(2)
.doesNotContainNull();
}
//팩토리 메소드
static Stream<Arguments> multiArgsProvider() {
return Stream.of(
arguments("apple", 6, Arrays.asList(1, 2)),
arguments("lemon", 7, Arrays.asList(3, 4))
);
}
Argument Conversion
JUnit은 제공된 argument에 대한 다양한 형 변환을 지원한다. 가장 기본적으로 @ValueSource
에서 지정한 int
형 배열의 각 요소를 테스트 메소드에서는 int
, long
, float
, double
타입의 파라미터로 받을 수 있다.
더 나아가 테스트 메소드의 파라미터 타입에 따라 묵시적인 형 변환도 가능하다. 예를 들어 String
타입의 argument를 Enum
, Boolean
, Integer
, LocalDate
등으로 자동 변환하여 테스트 메소드의 파라미터로 전달할 수 있다.
뿐만 아니라 직접 ArgumentConverter
를 별도로 정의하고 해당 테스트 메소드의 파라미터 앞에 @ConvertWith
어노테이션을 선언함으로써 전달된 argument를 지정한 컨버터를 통해 특정 타입으로 형 변환하는 것 또한 가능하다.
//기본적인 형 변환
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testWithConversion(double d) {
assertThat(d).isInstanceOf(Double.class);
}
//묵시적인 형 변환
@ParameterizedTest
@ValueSource(strings = {"2020-12-31", "2021-12-31", "2022-12-31"})
void testWithImplicitConversion(LocalDate localDate) {
assertThat(localDate).hasMonth(Month.DECEMBER)
.hasDayOfMonth(31);
}
//명시적인 형 변환
@ParameterizedTest
@ValueSource(strings = {"apple", "banana", "strawberry"})
void testWithExplicitConversion(@ConvertWith(ToLengthArgumentConverter.class) Integer length) { //String → Integer 형변환
assertThat(length).isGreaterThan(0);
}
//String → Integer Converter
public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {
protected ToLengthArgumentConverter() {
super(String.class, Integer.class);
}
@Override
protected Integer convert(String source) {
return (source != null ? source.length() : 0);
}
}
이렇게 JUnit의 Parameterized Test를 사용하면 더 깔끔하고 가독성 있는 테스트 코드를 작성할 수 있을 것이다. 사용법도 간단하지 않은가? 아무튼 지금까지 JUnit 테스트 프레임워크에 대해 알아보았는데, 이 정도면 단위 테스트 코드를 작성하는데에는 무리가 없을 것이라 생각된다. JUnit에 대해 추가로 공부하고 싶은 부분이 생긴다면 그때 가서 글을 더 작성하기로 하고, 다음으로는 Spring에서 이 단위 테스트를 어떻게 적용하는지, 개발한 API 별로 Web 계층에서는 어떻게 테스트 코드를 작성하는지 등에 대해 알아보려고 한다.
끝.
참고
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests
'자바 > 테스트' 카테고리의 다른 글
[Mockito] Spring Boot에서 Service 계층 테스트하기 (0) | 2023.10.08 |
---|---|
[Mockito] Mocking Framework with JUnit5 (1) | 2023.10.07 |
[JUnit] AssertJ Assertions (0) | 2023.06.23 |
[JUnit] JUnit5 Assertions (0) | 2023.06.20 |
[JUnit] 테스트 Tagging과 Filtering (0) | 2023.06.02 |
- Total
- Today
- Yesterday
- spring aop
- spring
- 모두의 리눅스
- Gitflow
- junit5
- JPA
- ParameterizedTest
- 디자인 패턴
- 단위 테스트
- Front Controller
- rest api
- C++
- Transaction
- vscode
- facade 패턴
- mockito
- Git
- servlet filter
- SSE
- QueryDSL
- Linux
- 템플릿 콜백 패턴
- Java
- 서블릿 컨테이너
- 전략 패턴
- Assertions
- FrontController
- Spring Security
- github
- spring boot
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |