middlefitting

Junit @Tag를 활용한 통합, 단위 테스트 분리 본문

TestCode

Junit @Tag를 활용한 통합, 단위 테스트 분리

middlefitting 2023. 12. 24. 15:25

스프링에서 테스트 코드를 작성하다 보면, 통합 테스트와 단위 테스트를 주로 작성하게 된다. 그런데 테스트 코드가 점점 많아지면서 관리에 어려움이 생긴다. 커버리지 측정에 있어 단위 테스트와 통합 테스트를 분리하고 싶은 경우도 생기고, 각각의 테스트를 분리해서 실행시키고 싶은 경우가 존재하는데 gradle에서 기본적으로 제공하는 Test 태스크는 모든 테스트를 한번에 실행시키기 때문에 파일의 구분이 어렵다.

 

근데 스프링과 JUnit에서는 당연히 해당 내용에 대한 해결책을 제시한다. 나와 비슷한 고민을 한 분들을 위해 gralde, JUnit, Spring에서 간단한 방법으로 단위 테스트와 통합 테스트를 구분하는 방법을 공유하고자 한다.

 

 


1) Tag 활용하기

@Tag

 

해결 방법은 아주 간단하다. JUnit에서 제공하는 애노테이션인 Tag 애노테이션을 활용하면 된다.

 

@SpringBootTest
@Tag("IntegrationTest")
public class Test1 {
	. . .
}


@Tag("UnitTest")
public class Test2 {
	. . .
}

우선 이렇게 테스트 클래스 레벨에 애노테이션을 붙여주면 해당 클래스에서 진행하는 테스트에 태그 정보를 넣어줄 수 있다.

 

 

Raw String은 한 곳에서 관리하자

 

그런데 테스트마다 저렇게 Raw String을 붙여주는 것은 좋지 않다. 오타가 날 수도 있고 혹시라도 저 태그명이 바뀌게 된다면 모든 태그명을 변경해야 하는 대참사가 발생하게 된다. 이런 상황에서는 enum을 활용하는 것이 좋지만 애노테이션에는 constant 내용을 넣어줘야 해서 enum의 필드를 넣어주기는 어렵다. 따라서 상수를 제공하는 별도의 클래스를 만들어서 관리할 수 있다.

 

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TestTypeConstants {
    public static final String UNIT_TEST = "UnitTest";
    public static final String INTEGRATION_TEST = "IntegrationTest";
}


@SpringBootTest
@Tag(TestTypeConstants.INTEGRATION_TEST)
public class Test1 {
	. . .
}


@Tag(TestTypeConstants.UNIT_TEST)
public class Test2 {
	. . .
}

String을 한곳에서 관리하므로 좀 더 안정적으로 태그를 붙일 수 있다. 그런데 이렇게 태그를 붙이는 것조차 번거로운 작업이 될 수 있다. 실수로 태그를 누락할 가능성도 있다. 여기서 Java/Spring의 꽃인 애노테이션으로 좀 더 편리하게 개선할 수 있다.

 

 

테스트 편의성 애노테이션 정의하기
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Tag(TestTypeConstants.UNIT_TEST)
public @interface UnitTest {

}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@Import(TestRedisConfig.class)
@ContextConfiguration(initializers = RedisInitializer.class)
@Tag(TestTypeConstants.INTEGRATION_TEST)
public @interface IntegrationTest {

}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@ContextConfiguration(initializers = RedisInitializer.class)
@Tag(TestTypeConstants.INTEGRATION_TEST)
public @interface IntegrationTestWithRedisTransaction {

}

 

현재 참여하고 있는 프로젝트에 작성한 애노테이션이다. 단위 테스트에 경우에는 보통 POJO를 테스트하니 불러올 설정이 거의 존재하지 않지만, 통합 테스트에 경우에는 참여하고 있는 프로젝트 환경에 따라 설정해 줘야 하는 내용이 많다. 해당 내용에서도 테스트 용으로 작성된 redis 설정 빈으로 바꿔치기 하고, redis 테스트 컨테이너를 띄우기 위한 빈을 불러오는 것을 확인할 수 있다. 또한 AOP를 통해 전후 작업을 설정하면서 테스트에 필요한 유용한 기능을 추가할 수도 있다.

 

@IntegrationTest
public class Test1 {
	. . .
}


@UnitTest
public class Test2 {
	. . .
}

커스텀 애노테이션을 추가하면서 이렇게 테스트 코드 작성을 위한 애노테이션을 줄일 수 있다. 

 

 


1) Gralde Task 활용하기

testImplementation 'org.springframework.boot:spring-boot-starter-test'


test {
   useJUnitPlatform()
}

이제 테스트 코드 상에서는 준비가 완료되었다. 다음은 Gradle을 설정해주면 된다. spring-boot-starter-test를 추가하였다면 기본적으로 test 태스크가 useJUnitPlatform을 사용한다는 것을 확인할 수 있을 것이다. Junit5를 사용한다음 의미이며 Junit4의 경우에는 useJunit()을 사용한다. test 태스크에 대한 자세한 내용은 아래 공식문서에서 확인할 수 있다.

 

https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/Test.html

 

Test (Gradle API 8.5)

Indicates if this task will skip individual test execution. For JUnit 4 and 5, this will report tests that would have executed as skipped. For TestNG, this will report tests that would have executed as passed. Only versions of TestNG which support native d

docs.gradle.org

 

 

커스텀 Test Task 설정하기

 

이제 Test 타입을 가지는 커스텀 태스크만 설정하면 된다. 아래와 같이 설정해주자.

 

//전체 테스트
test {
	description = 'Runs the total tests.'
	useJUnitPlatform()
}

//유닛 테스트
task unitTest(type: Test) {
	group = 'verification'
	description = 'Runs the unit tests.'
	useJUnitPlatform{
		includeTags 'UnitTest'
		excludeTags 'IntegrationTest'

	}
}

//통합 테스트
task integrationTest(type: Test) {
	group = 'verification'
	description = 'Runs the integration tests.'
	useJUnitPlatform{
		includeTags 'IntegrationTest'
		excludeTags 'UnitTest'
	}
}

저 태스크가 수행하는 의미를 설명하자면 다음과 같은 내용을 포함한다. test 테스크와 같이 verification 그룹으로 묶고, 간단한 설명을 추가한다. 이후 useJUnitPlatform 문법에 맞게 아까 정의한 어떤 Tag를 포함하고 어떤 태그를 제외할 것인지 설명을 넣어주는 것으로 unitTest, integrationTest 태스크가 간단히 생성된 것을 확인할 수 있다. 추가적인 useJUnitPlatform 에 대한 내용이 궁금하면 아래의 공식문서를 참고하면 된다.

 

https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/junitplatform/JUnitPlatformOptions.html

 

JUnitPlatformOptions (Gradle API 8.5)

Copies the options from the source options into the current one.

docs.gradle.org

 

 

커스텀 태스크 실행하기

 

태스크를 만들었으니 이제 그때그때 필요한 태스크를 실행하면 된다. 

 

 

다음과 같이 integrationTest와 unitTest 태스크가 생성된 것을 확인할 수 있다. coverage 태스크는 jacoco를 활용해서 해당 부분을 확장한 태스크들인데 다른 글에서 다루어 보고자 한다. 이제 gradlew 문법으로 터미널에서 태스크명으로 실행시키거나 verification에서 해당 태스크를 실행시키면된다.

 

단위 테스트 실행결과

 

통합 테스트 실행결과

 

태스크를 실행시켜보면 이렇게 테스트가 잘 구분되는 걸 확인할 수 있다.

 

 

 


 

스프링에서 간단하게 테스트를 분리하는 방법에 대해 알아보았다. 테스트를 분리하면 독립적으로 실행시킬 수 있고, 커버리지도 별도로 측정할 수 있다. 간단하지만 얻을 수 있는 결과가 꽤 크다. 테스트 코드 분리에 대해 고민이 있으신 분들에게 조금이나마 도움이 되길 바란다.