반응형
05-02 17:37
Today
Total
«   2024/05   »
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
관리 메뉴

개발하는 고라니

간단하게 UnitTest와 IntegrationTest 분리하기 (with. kotlin gradle dsl) 본문

Framework/Spring Boot

간단하게 UnitTest와 IntegrationTest 분리하기 (with. kotlin gradle dsl)

조용한고라니 2023. 2. 28. 02:01
반응형

specification]

framework - spring boot 2.6.3

test - Junit 5

들어가며

이제 애플리케이션을 개발하는데 있어 테스트는 선택이 아닌 필수가 되었으며, 작성을 용이하게 해주는 라이브러리 또한 플랫폼에 따라 많이 나와있다.

 

그만큼 개발자에게 테스트는 비교적 저렴한 비용으로 나의 코드가 안전함을 보장하는 이점이 있다.

 

대표적인 서적으로 Unit Test이 있고, 살짝 보았으나 역시 어려워 이해하는데 어려움이 있었다.

어쩃든, 테스트는 크게 단위 테스트와 통합 테스트로 나뉘는데 

일반적으로 code review를 요청하기 위해 Github에 PR(Pull Request)을 올리는데, 이때 기존에 작성된 테스트 코드들이 깨지는지, 빌드가 깨지는지 등을 pipeline으로 정의해두는 경우가 많다.

 

이때마다 통합테스트가 동작하면 시간도 오래걸리고, 환경에 따라 동작하지 않을 수도 있어 유닛테스트만 검증하는 편이다. (팀바팀이지만 통합 테스트는 개인 로컬에서 돌려보는 편이다.)

 

이 두 분류의 테스트를 어떻게 나눌 수 있을까?

Annotation - @Tag

Junit 5에서는 테스트 클래스나, 테스트 메서드에 태그를 붙일 수 있다.

즉, 다음과 같이 사용할 수 있다.

package com.gorany.user

import com.gorany.annotation.UnitTest
import org.apache.commons.lang3.StringUtils
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

@Tag("ATestClass")
internal class UserTest {

    @Test
    @DisplayName("계정 생성시 패스워드는 8자 이상이어야한다.")
    @Tag("ATest")
    fun when_create_user_then_password_length_must_longer_than_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = "0123456789"
        
        //when
        val result = User.of(email, password, name)

        //then
        assertThat(result.password.length).isGreaterThanOrEqualTo(8)
    }

    @Test
    @DisplayName("계정 생성시 패스워드가 8자 미만이면 예외가 발생한다.")
    @Tag("BTest")
    fun throw_exception_when_password_length_is_under_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = "0"

        //when
        assertThatThrownBy { User.of(email, password, name) }
            .isInstanceOf(IllegalArgumentException::class.java)
            .hasMessage("비밀번호는 8자 이상이어야 합니다.")
    }
}

간편하게 테스트 클래스와 메서드에 별명을 붙여준 것이라 보면된다.

Custom Annotation

위와 같이 하면 매번 붙이고자 하는 클래스나 메서드에 모두 태그를 붙여줘야하는 번거로움이 있어 커스텀 어노테이션을 만들어보려고 한다.

만들고자 하는 용도는 

  • Unit Test를 위한 어노테이션
  • Integration Test를 위한 어노테이션

이다.

// UnitTest Annotation
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockKExtension::class)
@Tag("unitTest")
@Retention(AnnotationRetention.RUNTIME)
annotation class UnitTest()


// IntegrationTest Annotation
import org.junit.jupiter.api.Tag
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
@Tag("integrationTest")
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.TYPE, AnnotationTarget.CLASS)
annotation class IntegrationTest()

이렇게 어노테이션을 만들어주었으면, 유닛 테스트를 모아놓은 테스트 클레스에 붙여보도록 하자.

 

@UnitTest
internal class UserTest {

    @Test
    @DisplayName("계정 생성시 패스워드는 8자 이상이어야한다.")
    fun when_create_user_then_password_length_must_longer_than_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = "0123456789"
        
        //when
        val result = User.of(email, password, name)

        //then
        assertThat(result.password.length).isGreaterThanOrEqualTo(8)
    }

    @Test
    @DisplayName("계정 생성시 패스워드가 8자 미만이면 예외가 발생한다.")
    fun throw_exception_when_password_length_is_under_eight() {

        //given
        val email = StringUtils.EMPTY
        val name = StringUtils.EMPTY
        val password = "0"

        //when
        assertThatThrownBy { User.of(email, password, name) }
            .isInstanceOf(IllegalArgumentException::class.java)
            .hasMessage("비밀번호는 8자 이상이어야 합니다.")
    }
}

 

이러면, UserTest라는 테스트 클레스는 "UnitTest" 라는 별명이 붙게되었다.

 

Test 명령어 실행하기

이제 command를 치면 된다.

./gradlew test

이렇게 하면 될까?

아니다.

gradle 설정을 해줘야한다.

 

// build.gradle.kts
    tasks.withType<Test> {
        useJUnitPlatform()
    }

    tasks.test {
        useJUnitPlatform() {
            includeTags("unitTest")
            excludeTags("integrationTest")
        }
    }

 

test 할때, "unitTest" 태그가 붙은 것을 포함하고, "integrationTest" 태그가 붙은 것을 제외한다는 설정이다.

 

이제 다시 명령어를 쳐보자.

./gradlew test

과연 유닛 테스트만 동작하는 것이 맞을까?

IntegrationTest 추가

검증해보자. 우선 예외를 던지는 integrationTest 아무거나 만든다.

@IntegrationTest
class SampleExceptionTest {

    @Test
    @DisplayName("예외가 발생하는 테스트")
    fun just_run_throw_exception() {

        throw RuntimeException("SAMPLE ERROR")
    }
}

 

만일, ./gradlew test 했을 때 "unitTest"가 붙은 테스트만 동작하는 것이 아니라면, 이 작업은 깨져야한다.

BUILD SUCCESSFUL in 8s
32 actionable tasks: 32 up-to-date

 

정상적으로 성공하는 것을 보니, 의도대로 잘 되고있다.

IntegrationTest만 수행하기

유닛테스트만 동작하도록 분리했으니, 통합 테스트도 동작할 수 있도록 설정을 해야겠다.

 

gradle의 test 명령어는 이미 unitTest만 동작하도록 점유하고 있으니, 'integration' 이라는 명령어로 통합테스트만 동작하도록 설정해보자.

    task<Test>("integration") {
        useJUnitPlatform() {
            excludeTags("unitTest")
            includeTags("integrationTest")
        }
    }

이후 명령어 실행

./gradlew integration
> Task :api:integration

SampleExceptionTest > 예외가 발생하는 테스트 FAILED
    java.lang.RuntimeException: SAMPLE ERROR
        at com.gorany.sample.SampleExceptionTest.just_run_throw_exception(SampleExceptionTest.kt:14)
        ...

 

의도한대로 SampleExceptionTest에서 테스트가 깨지는 것을 확인하였으니, integrationTest 태그가 붙은 친구를 동작시키는 것을 확인했다.

결론

이 방법 말고도, 디렉토리 구조로 나누는 방법 등 다양하게 있다. 하지만 나는 이처럼 태그를 가지고 하는 것이 가장 간편했다.

만약 편의를 위해 유닛/통합 테스트를 분리해야한다면 이 방법을 시도해보면 좋을 것 같다.

 

# Github

 

[add-unit-test] by rhacnddl · Pull Request #6 · rhacnddl/monorepo

[add-unit-test] unitTest / integrationTest 어노테이션 생성 UserTest 작성 gradle test 명령어 분리 (유닛/통합)

github.com

 

반응형
Comments