특정 메서드의 결과를 확인할 때 여러 단위 테스트를 만드는 경우가 있죠
fun isOdd(number: Int): Boolean {
return number % 2 != 0
}
간단하지만 위 메서드를 테스트하는 상황을 가정해 봅시다 🤔
인자로 들어온 숫자가 홀수일 경우 true를, 짝수일 경우는 false를 반환합니다
위 테스트를 어떻게 테스트 할 수 있을까요?
class OddTest {
@Test
fun `1은 홀수이다`() {
val case = 1
val actual = isOdd(case)
assertThat(actual).isTrue()
}
@Test
fun `3은 홀수이다`() {
val case = 3
val actual = isOdd(case)
assertThat(actual).isTrue()
}
@Test
fun `5은 홀수이다`() {
val case = 5
val actual = isOdd(case)
assertThat(actual).isTrue()
}
}
부끄럽지만 저는 테스트의 케이스가 달라지면 하나하나 테스트 메서드를 작성해 주었답니다
하지만 여러 케이스들에 대해 여러 테스트 메서드를 작성하지 않고,
하나의 테스트 메서드로 테스트 할 수 있는 방법이 있는데요,
바로 @ParameterizedTest를 이용하면 됩니다 💪
@ParameterizedTest를 사용하면 서로 다른 인수를 사용하여 테스트를 여러번 실행할 수 있습니다 ! 🔥
각 테스트를 실행할 때 사용할 인수들을 제공할 소스를 하나 이상 반드시 선언해 주어야 하고,
테스트 메서드 안에서 그 인수들을 사용해야 합니다
인수들은 @ValueSource, @NullSource, @EmptySource, @EnumSource, @MethodSource, @FieldSource, @CsvSource 등의 소스를 사용해 전달해 줄 수 있습니다
위 홀수 확인 메서드의 테스트 코드를, @ValueSource 소스를 이용해 테스트해 봅시다
@ParameterizedTest
@ValueSource(ints = [1, 3, 5])
fun `홀수이면 True를 반환한다`(number: Int) {
val actual = isOdd(number)
assertThat(actual).isTrue()
}
위처럼 인자로 넣어 줄 숫자들을 배열로 넣어 주면, 해당 배열의 원소만큼 테스트가 실행됩니다

실행해 보면 3개의 테스트가 모두 통과한 것을 볼 수 있습니다
💡 참고로
@ParameterizedTest의name을 지정해 주면, 각 테스트의 이름을 지정해 줄 수 있답니다 🤗
@ValueSource는 인수를 제공하는 가장 간단한 소스 중 하나입니다 !
이 어노테이션을 사용하면 하나의 리터럴 값 배열을 지정할 수 있으며, 파라미터화된 테스트가 실행될 때마다 단일 인수만을 제공합니다
💡 ValueSource에서 사용 가능한 타입엔 뭐가 있을까?
short,byte,int,long,float,double,char,boolean,java.lang.String,java.lang.Class
@ParameterizedTest의 인자로 null값이나, 빈 값을 이용하고 싶을 때 사용하는 소스입니다 ! 😎
@ParameterizedTest
@NullSource
@EmptySource
fun `문자열이 공백이거나 널값이다`(text: String?) {
assertTrue(text == null || text.trim().isEmpty())
}

@NullSource를 사용하려면 인자를 nullable 타입으로 받아 주어야겠죠? ❌
또한 아래처럼 @NullAndEmptySource를 사용하면, @NullSource와 @EmptySource를 중첩해 놓은 것과 같은 효과를 볼 수 있습니다 !
@ParameterizedTest
@NullAndEmptySource
fun `문자열이 공백이거나 널값이다`(text: String?) {
assertTrue(text == null || text.trim().isEmpty())
}
당연히 실행 결과는 동일합니다 👍🏻
인자로 enum 상수를 전달하고 싶을 때 사용하는 소스입니다 !
enum class Color {
RED,
GREEN,
BLUE,
}
class ColorTest {
@ParameterizedTest
@EnumSource(value = Color::class)
fun `enum 상수 테스트`(unit: Color) {
println(unit)
assertTrue(Color.entries.contains(unit))
}
}

이때 value 속성은 선택 사항입니다 🙃
생략할 경우 메서드의 첫번째 파라미터에 선언됩 타입이 사용되지만,
열거형이 아니라면 테스트에 실패하게 됩니다
위 예제의 경우 첫번째 파라미터 (unit: Color)가 Color 타입이므로, Color 클래스를 사용하겠네요 😵💫
또한 기본적으로 아무 속성도 지정하지 않는다면, enum에 정의된 모든 상수를 테스트하게 됩니다
하지만 우리가 원하는 상수만 테스트 할 수 있어야겠죠?
이럴 때 사용할 수 있는 속성으로 names과 mode가 있습니다 🔥

names 에 RED, GREEN을 지정해 준 경우, 각각에 대해서 테스트가 실행된 것을 볼 수 있습니다

위와 동일한 코드에서 mode를 EXCLUDE로 지정해 주니,
RED, GREEN을 제외한 BLUE에 대해 테스트가 실행되네요 👍🏻
mode에서 사용할 수 있는 상수로는 INCLUDE, EXCLUDE, MATCH_ALL, MATCH_ANY 등등이 있습니다
테스트 클래스나 외부 클래스에 있는 하나 이상의 팩토리 메서드를 참조할 수 있게 해 주는 소스입니다 !
외부 클래스의 팩토리 메서드는 항상 static이어야 하며,
테스트 클래스 내부의 팩토리 메서드는 클래스가 @TestInstance로 어노테이션되어 있지 않은 한,
반드시 static 이어야 합니다
( @TestInstance 의 역할은 다음에 더 탐구해 보는 걸로... )
또한 함수의 반환값은 Stream, Iterator, Iterable, Object 중 하나여야 합니다

이렇게 이름을 List<String>으로 반환해 주는 함수를 팩터리 메서드로 만들고 사용해 보면,
테스트가 잘 통과하는 것을 볼 수 있습니다

만약 외부 클래스의 메서드를 사용하고 싶다면,
위처럼 @MethodSource 안에 패키지명 + 클래스명 + 메서드명을 넣어 주어야 합니다
💡 왜 메서드에 @JvmStatic을 붙여야 할까?
위 어노테이션은 자바 기반으로 작성되어 있고, 내부적으로static method를 호출하여 매개변수를 생성합니다 🤔
다만 자바에서는kotlin의object의 메서드를static 변수처럼 접근하지 못하므로,@JvmStatic어노테이션이 필요하다고 합니다
_
간단히 보면 companion object 내부의 팩토리 메서드를 접근할 때,
자바에서는Class.Companion.Method로 접근하게 됩니다
하지만@JvmStatic을 붙여 주면Class.Method로 접근 가능합니다
(클래스 내부에서 Companion의 메서드에 접근하는 메서드를 하나 더 생성하는 원리)
마지막으로 인수 목록을 쉼표로 구분된 값들로 표현하는 @CsvSource입니다 ! 😵💫
@ValueSource의 경우 하나의 인수만 전달할 수 있기 때문에, 여러 개의 인수를 전달하고 싶을 때 사용합니다
value 속성을 통해 제공된 각 문자열은 하나의 CSV 레코드를 나타내며, 이는 파라미터화된 테스트를 한번 호출하는 결과로 이어집니다
@ParameterizedTest()
@CsvSource(value = ["apple, 1", "banana, 2", "'lemon, lime', 0xF1", "strawberry, 700_000"])
fun `CsvSource 테스트1`(fruit: String, rank: Int) {
println("$fruit - $rank")
}

또한 useHeaderInDisplayName과 textBlock을 사용해 가독성을 높여 줄 수도 있습니다 ! 🔥

@ParameterizedTest()
@CsvSource(value = ["apple:1", "banana:2", "'lemon, lime':0xF1", "strawberry:700_000"], delimiter = ':')
fun `CsvSource 테스트1`(fruit: String, rank: Int) {
println("$fruit - $rank")
}
delimeter 속성을 지정하여 구분자를 지정해 주는 것도 가능하답니다 🤗
이외에도 여러 속성들이 있으니 궁금하시다면 더 찾아보시는 걸 추천드립니다 👍🏻
📔 참고 자료
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests
ValueSource, CsvSource만 사용했었는데 다른 다양한 방식이 있었군요~