파라미터화된 테스트는 개발자들이 다양한 입력 값을 사용하여 코드를 효율적으로 테스트할 수 있게 합니다. JUnit 테스트의 세계에서, 오랜 경험을 가진 사용자들은 이러한 테스트를 구현하는 복잡성에 오랫동안 직면해 왔습니다. 하지만 JUnit 5.7의 출시로, 새로운 시대의 테스트 파라미터화가 도래하였고, 개발자들에게 최상급의 지원과 향상된 기능을 제공하고 있습니다. JUnit 5.7이 파라미터화된 테스트에 어떤 흥미로운 가능성을 가져다주는지 탐구해 보겠습니다!
JUnit 5.7 문서에서의 파라미터화 샘플
문서에서 몇 가지 예제를 보겠습니다:
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
@ParameterizedTest
어노테이션은 파라미터를 가져올 곳을 설명하는 여러 가지 제공된 원본 어노테이션 중 하나와 함께해야 합니다. 파라미터의 원본은 종종 “데이터 제공자”라고 불립니다.
I will not dive into their detailed description here: the JUnit user guide does it better than I could, but allow me to share several observations:
@ValueSource
는 단일 파라미터 값을 제공하는 데에 제한됩니다. 다시 말해, 테스트 메서드는 하나 이상의 인자를 가질 수 없으며, 사용할 수 있는 타입도 제한됩니다.- 여러 인자를 전달하는 것은
@CsvSource
를 통해 어느 정도 해결됩니다. 각 문자열을 레코드로 구분하여 필드별로 인자로 전달합니다. 하지만 긴 문자열이나 많은 인자가 있는 경우 읽기 어려워질 수 있습니다. 사용할 수 있는 타입도 제한적입니다 — 이에 대한 자세한 내용은 나중에 다룹니다. - 모든 소스는 어노테이션에서 실제 값을 선언할 때 컴파일 타임 상수로 제한됩니다 (Java 어노테이션의 제한으로, JUnit이 아님).
@MethodSource
와@ArgumentsSource
는 (타입이 없는) n-튜플의 스트림/коллекция를 제공하여 메서드 인자로 전달됩니다. n-튜플의 시퀀스를 나타내는 다양한 실제 타입이 지원되지만, 그 중 어떤 것도 메서드의 인자 목록에 맞을 것을 보장하지 않습니다. 이러한 종류의 소스는 추가 메서드나 클래스가 필요하지만, 테스트 데이터를 얻는 곳과 방법에 제한이 없습니다.
보시다시피, 사용 가능한 소스 유형은 간단한 것들(사용하기 쉽지만 기능이 제한적)에서 최종적으로 유연한 것들(작동하려면 더 많은 코드가 필요)으로 범위가 다릅니다.
- sideline — 이는 일반적으로 좋은 설계의 신호입니다: 필수 기능에 필요한 코드는 적고, 더 복잡한 사용 사례를 가능하게 하기 위해 추가 복잡성을 추가하는 것이 정당화됩니다.
이 가상의 간단함에서 유연함의 연속체에 맞지 않는 것은 @EnumSource
입니다. 각각 2개의 값을 가진 네 개의 파라미터 셋의 비트리비얼한 예를 보세요.
- note —
@EnumSource
는 열거형의 값을 단일 테스트 메서드 파라미터로 전달하지만, 개념적으로 테스트는 열거형의 필드로 파라미터화되어 있으며, 파라미터의 수에 제한이 없습니다.
enum Direction {
UP(0, '^'),
RIGHT(90, '>'),
DOWN(180, 'v'),
LEFT(270, '<');
private final int degrees;
private final char ch;
Direction(int degrees, char ch) {
this.degrees = degrees;
this.ch = ch;
}
}
@ParameterizedTest
@EnumSource
void direction(Direction dir) {
assertEquals(0, dir.degrees % 90);
assertFalse(Character.isWhitespace(dir.ch));
int orientation = player.getOrientation();
player.turn(dir);
assertEquals((orientation + dir.degrees) % 360, player.getOrientation());
}
생각해보면, 하드코딩된 값 목록은 유연성을 심각하게 제한합니다 (외부나 생성된 데이터 없음) 반면, enum
을 선언하는 데 필요한 추가 코드양은 @CsvSource
와 비교했을 때 매우 말뭉치인 대안입니다.
하지만 이는 첫 인상일 뿐입니다. 진정한 자바 enum의 강력한 기능을 활용할 때 얼마나 우아해질 수 있는지 보겠습니다.
- aside : 이 글은 제품 코드의 일부인 enum의 검증에 대해 다루지 않습니다. 물론, 어떻게 검증하든 enum은 선언되어야 했습니다. 대신, 이 글은 언제와 어떻게 테스트 데이터를 enum 형태로 표현할지에 대해 집중합니다.
언제 사용할 것인가
enum이 다른 대안보다 더 나은 성능을 발휘하는 상황이 있습니다:
테스트당 여러 개의 매개변수
단일 매개변수만 필요할 때는 @ValueSource
를 복잡하게 만들고 싶지 않을 것입니다. 하지만 입력과 기대 결과와 같은 여러 개의 매개변수가 필요하면 @CsvSource,
@MethodSource/@ArgumentsSource
또는 @EnumSource
로 전환해야 합니다.
어떤 의미에서 enum
은 여러 개의 데이터 필드를 “smuggle in”할 수 있게 합니다.
따라서 미래에 테스트 메서드 매개변수를 추가해야 할 때, 기존 enum에 더 많은 필드를 추가하면 됩니다. 테스트 메서드 서명은 건드리지 않습니다. 여러 테스트에서 데이터 공급자를 재사용할 때 이는 귀중합니다.
다른 소스에서는 ArgumentsAccessor
또는 ArgumentsAggregator
를 사용해야 하며, 이는 enum이 기본적으로 제공하는 유연성을 활용하기 위해서입니다.
타입 안전성
자바 개발자들에게 이는 매우 중요한 사항입니다.
CSV(파일 또는 리터럴)에서 읽은 파라미터, @MethodSource
또는 @ArgumentsSource
는 컴파일 시간에 파라미터의 개수와 타입이 서명과 일치할 것이라는 보장을 제공하지 않습니다.
물론, JUnit은 런타임에서 불만을 제기하지만 IDE에서 코드 지원은 기대할 수 없습니다.
이전과 같이, 동일한 파라미터를 여러 테스트에서 재사용할 때 이 문제가 더 커집니다. 타입 안전한 접근 방식을 사용하면 미래에 파라미터 셋을 확장할 때 큰 이점을 제공할 것입니다.
사용자 정의 타입
이는 주로 CSV에서 데이터를 읽는 텍스트 기반 소스에 대한 장점입니다 — 텍스트에 인코딩된 값은 자바 타입으로 변환되어야 합니다.
CSV 레코드에서 인스턴스화할 사용자 정의 클래스가 있다면 ArgumentsAggregator
를 사용하여 이를 수행할 수 있습니다. 그러나 데이터 선언은 여전히 타입 안전하지 않습니다 — 메서드 서명과 선언된 데이터 간의 불일치는 “인수를 종합할” 때 런타임에서 나타납니다. 또한, 에이그리게이터 클래스를 선언하는 것은 파라미터화 작업을 수행하기 위해 필요한 추가 지원 코드를 추가합니다. 그리고 우리는 @CsvSource
를 @EnumSource
보다 선호하여 추가 코드를 피했습니다.
문서화 가능성
다른 방법과 달리, 열거형 원본은 매개변수 쌍(열거형 인스턴스)과 그들이 포함하는 모든 매개변수(열거형 필드)에 대한 자바 기호를 모두 가지고 있습니다. 이는 문서를 더 자연스러운 형태로 첨부할 수 있는 직관적인 장소를 제공합니다 — 자바Doc.
문서를 다른 곳에 두는 것이 불가능한 것은 아니지만, 그것은 정의상 문서화된 대상에서 더 멀리 떨어져 있으며 따라서 찾기 어려워지고 오래되기 쉬워집니다.
하지만 더 있습니다!
이제: 열거형.은. 클래스.
많은Junior 개발자들이 자바 열거형이 진정으로 얼마나 강력한지 깨닫지 못한 것 같습니다.
다른 프로그래밍 언어에서는 정말로 단순한 상수로만 쓰이지만, 자바에서는 flyweight 디자인 패턴의 편리한 작은 구현으로 (대부분의) 완전한 클래스의 장점을 가지고 있습니다.
왜 그것이 좋은 일인가?
테스트 Fixture 관련 행동
다른 클래스와 마찬가지로, 열거형에는 메서드를 추가할 수 있습니다.
열거형 테스트 매개변수가 테스트 간에 재사용되는 경우, 같은 데이터로 조금 다른 방식으로 테스트하는 것이 편리해집니다. 중요한 복사와 붙여넣기를 피하고 매개변수를 효과적으로 작동시키기 위해서는 해당 테스트 간에 공유되는 일부 헬퍼 코드가 필요합니다.
헬퍼 클래스와 몇 가지 정적 메서드가 “해결”하지 못할 것은 아닙니다.
- 서론: 이러한 설계는 기능 탐내 문제를 겪습니다. 테스트 방법 – 혹은 더 나쁜 경우, 헬퍼 클래스 메서드 -는 enum 객체에서 데이터를 꺼내서 그 데이터에 작업을 수행해야 합니다.
이는 절차적 프로그래밍에서 (단지) 방법이지만, 객체 지향 세계에서는 더 나은 방법을 사용할 수 있습니다.
enum 선언 자체에 “헬퍼” 메서드를 선언하여, 데이터가 있는 곳으로 코드를 이동시킬 수 있습니다. 혹은 객체 지향 프로그래밍 용어로 표현하면, 헬퍼 메서드는 enum으로 구현된 테스트 fixture의 “행동”이 됩니다. 이렇게 하면 코드가 더 이디omat적이 되고 (인스턴스에 합리적인 메서드를 호출하는 것보다 정적 메서드를 통해 데이터를 전달하는 것보다), enum 매개변수를 테스트 케이스 간에서 재사용하기도 더 쉬워집니다.
상속
Enums는 (기본) 메서드가 있는 인터페이스를 구현할 수 있습니다. 적절하게 사용하면, 여러 데이터 제공자 – 여러 enum 간의 행동을 공유하는 데 활용할 수 있습니다.
가장 먼저 떠오르는 예시는 양수와 음수 테스트에 대한 별도의 enum입니다. 이들이 비슷한 종류의 테스트 fixture를 나타내는 경우, 공유할 수 있는 행동이 있을 가능성이 높습니다.
이야기는 싸다
이제 가상의 소스 코드 파일 변환기의 테스트 케이스에서 이를 설명해보겠습니다. 파이썬 2에서 3으로의 변환을 수행하는 것과 유사한 변환기입니다.
실제로 이러한 종합적인 도구가 무엇을 하는지에 대한 신뢰를 가지려면, 언어의 다양한 측면을 나타내는 광범위한 입력 파일 세트를 만들게 되며, 변환 결과를 비교하기 위한 매칭 파일을 만들어야 합니다. 그 외에도, 문제적인 입력에 대해 사용자에게 어떤 경고/오류가 제공되는지 확인해야 합니다.
이는 매우 많은 샘플을 확인해야 하므로 매개변수화된 테스트에 자연스럽게 맞아들어 맞지만, 데이터가 상당히 복잡하기 때문에 간단한 JUnit 매개변수 소스에 맞지 않습니다.
다음을 보세요:
enum Conversion {
CLEAN("imports-correct.2.py", "imports-correct.3.py", Set.of()),
WARNINGS("problematic.2.py", "problematic.3.py", Set.of(
"Using module 'xyz' that is deprecated"
)),
SYNTAX_ERROR("syntax-error.py", new RuntimeException("Syntax error on line 17"));
// 많은 다른 것들 ...
@Nonnull
final String inFile;
@CheckForNull
final String expectedOutput;
@CheckForNull
final Exception expectedException;
@Nonnull
final Set expectedWarnings;
Conversion(@Nonnull String inFile, @Nonnull String expectedOutput, @NotNull Set expectedWarnings) {
this(inFile, expectedOutput, null, expectedWarnings);
}
Conversion(@Nonnull String inFile, @Nonnull Exception expectedException) {
this(inFile, null, expectedException, Set.of());
}
Conversion(@Nonnull String inFile, String expectedOutput, Exception expectedException, @Nonnull Set expectedWarnings) {
this.inFile = inFile;
this.expectedOutput = expectedOutput;
this.expectedException = expectedException;
this.expectedWarnings = expectedWarnings;
}
public File getV2File() { ... }
public File getV3File() { ... }
}
@ParameterizedTest
@EnumSource
void upgrade(Conversion con) {
try {
File actual = convert(con.getV2File());
if (con.expectedException != null) {
fail("No exception thrown when one was expected", con.expectedException);
}
assertEquals(con.expectedWarnings, getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
} catch (Exception ex) {
assertTypeAndMessageEquals(con.expectedException, ex);
}
}
enum의 사용은 데이터가 얼마나 복잡할 수 있는지에 제한을 두지 않습니다. 보시다시피, enum에서 여러 가지 편리한 생성자를 정의할 수 있으므로, 새로운 매개변수 세트를 선언하는 것이 깨끗하고 간결합니다. 이는 많은 “빈” 값을(널, 빈 문자열, 또는 컬렉션) 포함하여 7번째 인수가 실제로 무엇을 의미하는지 의문시키는 긴 인수 목록의 사용을 방지합니다.
enum이 복잡한 유형(Set
, RuntimeException
)을 제한 없이 사용할 수 있도록 하며, 이러한 데이터를 전달하는 것도 완전히 타입 안전합니다.
이제 당신이 무엇을 생각하는지 알고 있습니다. 이는 아주 길어 보입니다. 그러나 어느 정도까지는 그렇습니다. 현실적으로 많은 데이터 샘플을 확인해야 하므로, 보일러플레이트 코드의 양은 비교적 적지 않을 것입니다.
또한, 동일한 enum과 그 헬퍼 메서드를 활용하여 관련 테스트를 작성하는 방법을 보세요:
@ParameterizedTest
@EnumSource
// 업그레이드된 파일을 다시 업그레이드하면 항상 통과하며 변경 사항이 없고 경고를 발하지 않습니다.
void upgradeFromV3toV3AlwaysPasses(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV3File());
}
@ParameterizedTest
@EnumSource
// 업그레이드 절차로 만들어진 파일을 다운그레이드 하면 경고 없이 항상 통과할 것으로 기대됩니다.
void downgrade(Conversion con) throws Exception {
File actual = convert(con.getV3File());
assertEquals(Set.of(), getLoggedWarnings());
new FileAssert(actual).isEqualTo(con.getV2File());
}
더 많은 이야기 후에
개념적으로, @EnumSource
는 각 테스트 시나리오에 대한 복잡하고 기계가 읽을 수 있는 설명을 작성하도록 장려하여 데이터 제공자와 테스트_fixture 간의 경계를 흐리게 합니다.
각 데이터 셋을 자바 심볼(enum 요소)로 표현하는 또 다른 큰 장점은 그들单个로 사용할 수 있다는 점입니다; 데이터 제공자/파라미터화된 테스트 외적으로. 이들은 합리적인 이름을 가지고 있고 자체 포함되어 있기 때문에(데이터와 행동 면에서), 아름답고 가독성이 좋은 테스트에 기여합니다.
@Test
void warnWhenNoEventsReported() throws Exception {
FixtureXmls.Invalid events = FixtureXmls.Invalid.NO_EVENTS_REPORTED;
// read()는 모든 FixtureXmls에서 공유되는 헬퍼 메서드입니다.
try (InputStream is = events.read()) {
EventList el = consume(is);
assertEquals(Set.of(...), el.getWarnings());
}
}
이제, @EnumSource
는 가장 자주 사용할 argument sources 중 하나가 되지 않을 것입니다. 그것은 좋은 일입니다. 과도하게 사용하면 좋지 않을 것이기 때문입니다. 하지만 적절한 상황에서는 그들이 제공하는 모든 것을 어떻게 사용하는지 알아 두는 것이 유용할 수 있습니다.아니
Source:
https://dzone.com/articles/junit5-parameterized-tests-with-enumsource