Right now do !

[JAVA] JUnit5-@ParameterizedTest

by 지금당장해

프롤로그

요즘 IDE 또는 그 확장 솔루션들 예를들어 SonarLint 등이 아주 아주 성능이 좋아져서 중복된 코드나 복잡도가 높은 코드를 발견하면 잔소리를 늘어 놓는다. 사람이 하는 잔소리라면 한번 듣고 넘어가겠지만 계속 노란줄로 너 이거 “계속 안고칠꺼야?” 하고 경고를 한다. 테스트를 작성하다 보면 흐름은 거의 비슷하고 데이터 만 바뀌고 여기에 따라서 단정문도 살짝 바뀌는 수준의 테스트 메서드를 여러개 작성하게 된다. 이 경우 요즘 정적 분석기는 이런 중복도 잡아 준다. 뭐 정적 분석기가 잔소리를 하던 말던 제품 코드 뿐만 아니라 테스트 코드도 중복되어 좋을 것이 없다. 오늘은 이 문제를 해결하기 위한 방법을 하나 소개한다.

Parameterized test

궂이 우리 말로 번역하자면 인자화 테스트 … 우리가 일반적으로 알고 있는 테스트 메서드 즉 @Test 애노테이션을 적용하는 테스트는 반환 값도 인자도 없다. 테스트 메서드가 인자에 해당하는 테스트 데이터도 반환의 소비 즉 테스트 단정도 다 알아서 하라는 것이다. TDD 사상에 입각해서 생각해보면 원칙은 그렇다. 필자는 이를 테스트의 독립성이며 단위성이라고 부른다. 그래서 각 테스트 메서드가 알아서 다 하게 작성하다 보면 서두에서 언급한 테스트 구현 코드내에 중복이 발생 하는 것이다. 이 두가지 원칙을 다 준수 할 방법이 없을까? 고민 끝에 그래 이참에 이거 써보자 했던 것이 바로 … @ParameterizedTest 이다.

@ValueSource

단순한 값의 나열을 통해 테스트 데이터(케이스)를 열거하여 테스트 해야 하는 경우 적용되며. 다음과 같이 사용한다.

    @ParameterizedTest
    @ValueSource(strings = {"", "qwerasdfzxcv", "http://www.example.com:-80", "htp://www.example.com"})
    void testGetResponseByHttpRequest_MalformedUrl(String url) {

        assertThrows(MalformedURLException.class,
            ()->HttpClient.getResponseByHttpRequest(url,true));


    }

위 테스트는 이상한 URL을 사용하면 특정 메서드가 MalformedURLException 예외를 의도 대로 잘 throw하는 검증하는 테스트 이다. 잘못된 url은 다양한 이유로 존재 이를 @ValueSource 애노테이션의 인자(strings = {"", "qwerasdfzxcv"})로 나열한다. 그리고 이 테스트 매서드는 드디어 기존 틀에서 벗어나 인자를 소유하게 된다. String url 이라고 떡 인자가 정의 되어 있다. 이 테스트 코드를 수행하면 IDE에서 다음 그림과 같은 결과를 볼수 있다.

테스트 결과1

테스트 메서드는 하나이지만 테스트 수행은 각 데이터 별로 N 번 수행 되는 것을 알 수 있다. 1타 N피다. 코드는 간결해지고 층가하는 테스트 데이터(캐이스)에 빠르게 대처 할 수 있다. 만약 인자가 위와 같이 String이 아니라 숫자여야 한다면 ints={테스트 데이터 나열} 를 사용하면 된다.

@MethodSource

만약 데이터가 하나가 아니라 복합적인 경우 그리고 이에 따른 단정 값 또한 다른경우 @ValueSource 만으로는 한계를 가지고 있다.  다음은 복합적인 테이터를 테스트 메서드에 공급 해야 하는 경우 제 3의 메서드를 활용하여 테스트 코드의 중복을 회피하는 예제이다.

@ParameterizedTest
@ExtendWith(SslEnvironmentExtension.class)
@MethodSource("provideParameters")
void testGetRequest_ssl(boolean useSsl, boolean useHttp2, String protocolVersion)
    throws ConfigurationException, LifecycleException, ClassNotFoundException {

    final String hostName = "www.potalab.com";

    Connector connector = startConnector(hostName, "HTTP", useSsl, useHttp2, CERT_PATH);
    AtomicReference<HttpClientResponse> responseResult = new AtomicReference<>();
    assertDoesNotThrow(()->requestToServer(hostName, useSsl, useHttp2, responseResult));
    connector.waitOnStarted();

    assertNotNull(responseResult.get());
    assertEquals(200, responseResult.get().getCode());
    assertEquals("HTTP", responseResult.get().getProtocol());
    assertEquals(protocolVersion, responseResult.get().getProtocolVersion());
    Diagnostic.trace(responseResult.get().getBody());
}

private static Stream<Arguments> provideParameters() {
    return Stream.of(
        // useSsl, useHttp2, protocolVersion
        Arguments.of(true, false, "1.1"),
        Arguments.of(true, true, "2.0"),
        Arguments.of(false, true, "2.0")
    );
}

 

위 테스트 메서드는 Http Request를 함에 있어 총 2가지의 조건을 가지고 있다. SSL 적용유무와 HTTP2 프로토콜 적용 유무이다. 그리고 HTTP2 적용 유무에 따라 리스폰스 값에서 HTTP 프로토콜 버전이 다르게 수신됨을 확인 해야 하는 미션이 있다. @MethodSource("provideParameters") 애노테이션에 주목하자. 테스트 데이터의 나열 없이 이 테스트 클래스에 선언된 정적 메서드 하나를 지명하고 있다. 나는 여기서 데이터를 받을 꺼야 라고….. 20라인 부터 정의된 데이터를 공급하는 메서드를 보면 Arguments 라고 하는 타입의 Stream을 반환하고 있다. 다시 테스트 메서드를 보면 여기서 공급하는 데이터를 받아 사용할 수 있도록 인자 3개를 나열하고 있다. 테스트 데이터 2개에 단정용 데이터 1개를 인자로 받는다.

Arguments 사용을 위한 참조 추가: 추가로 JUnit5 계열의 패키지가 하나더 필요하다. (버전은 사용하고 있던 API에 맞춰서 알아서 하면 됨)

 

maven

<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.6.3</version>
<scope>test</scope>
</dependency>

gradle

// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.6.3'

 

이 테스트의 결과는 다음과 같다.

에필로그

테스트 코드를 작성하는 일은 상당히 지루하고 고된 일이다. 어쩌면 이보다 더 힘든일은 추가로 요구되는 사양 변화에 따른 테스트 케이스의 증가이다. 이 때 마다 메서드를 늘려 나가다 보면 나중에는 유지하기가 부담스러운 양과 질이 될것이 불보듯 뻔하다.

이를 고민하는 차원에서 정리를 해봤다.

블로그의 정보

지금 당장 해!!!

지금당장해

활동하기