본문 바로가기

IT/TDD & Test

[JUnit] Dynamic Tests

반응형
 

* JUnit5 사용

 

1.  Dynamic Test란?

* @Test 주석이 달린 표준 테스트는 컴파일 시간에 정의되는 static 한 테스트이다.

* Dynamic Test는 런타임 중에 생성되는 테스트

* @TestFactory 주석이 달린 팩토리 메소드에 의해 생성

* 팩토리 메소드는 DynamicTest 인스턴스의 Stream, Collection, Iterator를 반환해야 함

* @BeforeEach / @AfterEach 와 같은 생명주기 관련 어노테이션을 지원하지 않음

 

2. Dynamic Test 예제

* @TestFactory method는 dynamic test라는 것을 명시해준다.

* dynamic test는 컬렉션을 반환함

* 이름, 실행 두 부분으로 나뉨

@TestFactory
fun dynamicTestWithCollection(): List<DynamicTest> {
    return listOf(
        DynamicTest.dynamicTest("Add test") {
            assertEquals(2, Math.addExact(1, 1))
        },
        DynamicTest.dynamicTest("Multiply Test") {
            assertEquals(4, Math.addExact(2, 2))
        }
    )
}

* 전달된 이름을 출력한다.

 

 

* Iterable , Iterator, Stream 반환 하도록 작성 가능

@TestFactory
fun dynamicTestsWithIterable(): Iterable<DynamicTest> {
    return listOf(
        DynamicTest.dynamicTest("Add test") {
            assertEquals(2, Math.addExact(1, 1))
        },
        DynamicTest.dynamicTest("Multiply Test") {
            assertEquals(4, Math.addExact(2, 2))
        }
    )
}

@TestFactory
fun dynamicTestsWithIterator(): Iterator<DynamicTest> {
    return listOf(
        DynamicTest.dynamicTest("Add test") {
            assertEquals(2, Math.addExact(1, 1))
        },
        DynamicTest.dynamicTest("Multiply Test") {
            assertEquals(4, Math.addExact(2, 2))
        }
    ).iterator()
}

@TestFactory
fun dynamicTestsFromIntStream(): Stream<DynamicTest> {
    return IntStream.iterate(0) { n -> n + 2 }
        .limit(10)
        .mapToObj { n ->
            DynamicTest.dynamicTest("test $n") { assertTrue(n % 2 == 0) }
        }
}

 

3. DynamicTests Stream

* 도메인 이름을 입력하면 IP를 반환하는 예제를 작성해보자

* DomainNameResolver라는 가상의 라이브러리를 작성해 IP를 반환 받음

class DomainNameResolver {
    private val ipByDomainName: MutableMap<String, String> = HashMap()

    init {
        ipByDomainName["https://www.naver.com"] = "23.220.128.185"
        ipByDomainName["https://www.google.com"] = "142.250.217.100"
        ipByDomainName["https://www.daum.net"] = "211.249.220.24"
    }
    fun resolveDomain(domainName: String): String? {
        return ipByDomainName[domainName]
    }
}

 

* @TestFactory method에서 Stream을 반환하도록 작성

public class DynamicTest extends DynamicNode {
	...
    public static <T> Stream<DynamicTest> stream(Iterator<T> inputGenerator,
			Function<? super T, String> displayNameGenerator, ThrowingConsumer<? super T> testExecutor) {
	...                
}

 

* inputGenerator는 도메인 주소를 하나씩 반환.

* displayNameGenerator는 테스트 케이스의 고유한 이름을 제공한다.

* testExecutor는 DomainNameResolver을 활용해 inputGenerator의 값에 대응하는 도메인 값들을 생성하고 outputList에 대한 assertion을 생성한다.

 

@TestFactory
fun dynamicTestsFromStream(): Stream<DynamicTest> {

    val inputList: List<String> = listOf("https://www.naver.com", "https://www.google.com", "https://www.daum.net")
    val outputList: List<String> = listOf("23.220.128.185", "142.250.217.100", "211.249.220.24")

    val domainNameResolver = DomainNameResolver()

    val inputGenerator: Iterator<String> = inputList.iterator()
    val displayNameGenerator = Function<String, String> { name -> "Resolving: $name" }
    val testExecutor = ThrowingConsumer { input: String ->
        val index = inputList.indexOf(input)
        assertEquals(outputList[index], domainNameResolver.resolveDomain(input))
    }

    return DynamicTest.stream(
        inputGenerator, displayNameGenerator, testExecutor
    )
}

 

* inputList로부터 생성한 stream을 map을 통해 DynamicTest Stream으로 전환하여 반환하는 방법도 있음

@TestFactory
fun dynamicTestsFromStreamSimple(): Stream<DynamicTest> {
    val inputList: List<String> = listOf("https://www.naver.com", "https://www.google.com", "https://www.daum.net")
    val outputList: List<String> = listOf("23.220.128.185", "142.250.217.100", "211.249.220.24")

    val domainNameResolver = DomainNameResolver()

    return inputList.stream()
        .map { DynamicTest.dynamicTest("Resolving: $it") {
                val id = inputList.indexOf(it)
                assertEquals(outputList[id], domainNameResolver.resolveDomain(it))
            }
        }
}

 

* 조금더 응용한다면 input과 output을 정해두고 테스트케이스를 작성할 수 있다.

@TestFactory
fun dynamicTestsFromStreamCase(): Stream<DynamicTest> {
    class Case(
        val domain: String,
        val ip: String,
        val expected: Boolean
    )

    val domainNameResolver = DomainNameResolver()

    return listOf(
        Case("https://www.naver.com", "23.220.128.185", true),
        Case("https://www.google.com", "142.250.217.100", true),
        Case("https://www.daum.net", "wrong ip", false)
    ).stream().map {
        DynamicTest.dynamicTest("domain : $it") {
            assertEquals(it.expected, it.ip == domainNameResolver.resolveDomain(it.domain))
        }
    }
}

 

4. 마치며

 이러한 동적 테스트 케이스는 여러가지 이점을 줄수있다. 개인적으로는 구현에 집중하지 않고 설계에 더 집중할 수 있다고 생각한다.

동적 테스트를 짜다보면 구현에  종속적 테스트 코드는 작성하기 어렵고 그렇게 되면 다시한번 자신의 코드를 볼수있지 않을가 생각한다.

스프링캠프 2019 [Track 2 Session 3] : 무엇을 테스트할 것인가? 어떻게 테스트할 것인가? (권용근) :  https://www.youtube.com/watch?v=YdtknE_yPk4

반응형