Junit5 한번에 끝내기
소개
junit은 단위 테스트를 작성하는 자바 개발자 93%가 사용하는 테스팅 프레임워크 이다. 테스트 코드를 작성해야하는
구성
- JUnit Platform : 테스트를 실행시켜주는 런처를 제공, TestEngine API가 제공된다.
- Jupiter: JUnit 5를 지원하는 TestEngine API의 구연체
- Vintage: JUnit 4, 3를 지원하는 TestEngine API의 구연체
의존성 추가
- 2.2+ 버전의 스프링 부트 프로젝트라면
spring-boot-starter-test
에 기본으로 탑제가 되어있다. - 아니라면…
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.5.2</version> </dependency>
기본 에노테이션은
- @Test
- @BeforeAll / @AfterAll : 전체 @Test 메소드 전후
- @BeforeEach / @AfterEach : 각 @Test 메소드 전후
- @Disabled : @Test 메소드 실행 안하기
예제
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class StudyTest {
@Test
void test1() {
System.out.println("test 1");
}
@Test
void test2() {
System.out.println("test 2");
}
@Test @Disabled
void test3() {
System.out.println("test 3");
}
@Test
void create() {
Study study = new Study();
assertNotNull(study);
}
@BeforeAll
static void beforAll() {
System.out.println("before All");
}
@AfterAll
static void afterAll() {
System.out.println("after All");
}
@BeforeEach
void beforeEach() {
System.out.println("before Each");
}
@AfterEach
void afterEach() {
System.out.println("after Each");
}
}
결과 :
before All
before Each
after Each
before Each
test 1
after Each
before Each
test 2
after Each
after All
테스트 이름 표시
@Test
void test1() {
System.out.println("test 1");
}
위의 코드가 있을 때 기본 이름은 메소드명 이다.
이름 표기 방법을 설정
@DisplayNameGeneration
에노테이션을 사용하는데 Class와 Method에 붙여서 테스트 이름을 표기 방법을 설정 한다.
구현체는 ReplaceUnderscores
에서 제공한다.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@Test
void test_new_class() {
System.out.println("test 1");
}
}
위와 같이 작성할 수 있고 테스트 이름은 test new class
가 된다.
이름 설정 (추천, 이모지 사용가능)
@DisplayName
에노테이션을 Method에 붙여서 테스트 이름 설정할 수 있다.
@DisplayNameGeneration
보다 우선순위가 높다.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@DisplayName("테스트 \uD83D\uDE00")
@Test
void test_new_class() {
System.out.println("test 1");
}
}
assert API
테스트 코드라면 당연 Assert를 써서 값이 제대로 됬는지 확인해야한다.
Java assert 예약어를 사용해도 되지만 그것보다는 좀 더 다양한 기능을 제공하는 Junit의 assert API들이 있다.
JUnit5의 org.junit.jupiter.api.Assertions.*
에 다양한 Assert들이 있다.
자주 사용하는 것들만 보자면 …
-
기본적인 assert
assertEquals("예상하는 값", "결과", ()->"에러 메시지"); assertNotNull(new String(), ()->"에러 메시지"); assertNull(null, ()->"에러 메시지"); assertTrue(1 > 0, ()->"에러 메시지"); assertFalse(1 < 0, ()->"에러 메시지");
“에러 메시지"를 굳이 람다식으로 작성하지 않아도 되지만 에러 메시지의 연산이 있을 경우
람다식으로 작성하여 함수화 시키면 실질적으로 에러 메세지를 보여줘야 할 때만 보여준다. -
모든 assert 한번에 계산 assert를 사용하면 위의 assert가 False라는게 밝혀지만 아래 assert는 실행하지 않는데..
이걸assertAll()
로 해결할 수 있다.assertAll(() -> { assertTrue(1 < 0, ()->"에러 메시지"); assertFalse(1 > 0, ()->"에러 메시지"); });
-
Exception 체크
assertThrows
을 사용하면 Exception을 체크할 수 있다.IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { throw new IllegalArgumentException("test"); }); assertEquals("test", exception.getMessage());
-
시작 체크
assertTimeout
,assertTimeoutPreemptively
을 사용하면 작동되는 시간을 측정할 수 있다.
둘중에서assertTimeoutPreemptively
을 사용하는 것을 권장한다 타입아웃이 되는 경우assertTimeout
는 종료해버리지만assertTimeoutPreemptively
는 끝내버림으로 테스트 속도향상을 할 수 있다. 단, ThreadLocal을 제대로 모른다면 쓰지마라! 특히 transaction 같은 것에assertTimeoutPreemptively(Duration.ofMillis(100), () -> { Thread.sleep(1000); }); assertTimeout(Duration.ofMillis(100), () -> { Thread.sleep(1000); });
테스트에 조건에 따라 실행
특정 OS, Java Version, 실행 변수등에 따라 실행할지 안할지 지정할 수 있다.
-
코드로 하는 방법은
org.junit.jupiter.api.Assumptions.*
의 API를 사용하면 된다.@Test void test1() { assumeTrue("LOCAL".equals(System.getenv("TEST_ENV"))); assertTrue(false); //TEST_ENV값이 LOCAL이 아닐 경우 실행 안함 }
혹은
assumingThat("LOCAL".equals(System.getenv("TEST_ENV")), () -> { assertTrue(false); });
-
에노테이션으로 하는 방법은
@Enabled___
와@Disabled___
로 시작해- OnOS
- OnJre
- IfSystemProperty
- IfEnvironmentVariable 를 붙여서 쓰면 된다.
@EnabledOnOs({OS.WINDOWS, OS.MAC}) void test() { System.out.println("test 2"); }
테스트 태깅과 필터링
태깅은 우리의 테스트에 Tag를 걸 수 있다.
org.junit.jupiter.api.tag
혹은 tags
를 사용하면 된다.
@Tag("fast")
@Test
void test1() {
System.out.println("fast test");
}
이렇게 Tag는 필터를 통해 특정한 테스트만 실행할 수 있다.
이외에는
- https://maven.apache.org/guides/introduction/introduction-to-profiles.html
- https://junit.org/junit5/docs/current/user-guide/#running-tests-tag-expressions
커스텀 에노테이션
기본적은 JUnit5 jupiter 메타 에노테이션을 가지고 커스텀 에노테이션을 만들 수 있다.
@Tag("custome")
@Test
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomeTest {
}
사용할때는
@CustomeTest
void customeTest() {
System.out.println("Custome Test");
}
단순 테스트 반복
@RepeatedTest
에노테이션을 사용하면 된다.
단순하게는
@RepeatedTest(value = 10)
void repeatedTest(RepetitionInfo repetitionInfo) {
System.out.println(repetitionInfo.getCurrentRepetition() + " of " + repetitionInfo.getTotalRepetitions());
}
RepetitionInfo
라는 메소드를 통해 우리가 반복하려는 횟수와 토탈횟수등을 받을 수 있다.
테스트를 좀 더 이쁘게 꾸미려면…
@DisplayName("반복")
@RepeatedTest(value = 10, name = "{displayName} {currentRepetition} / {totalRepetitions}")
void repeatedTest(RepetitionInfo repetitionInfo) {
System.out.println(repetitionInfo.getCurrentRepetition() + " of " + repetitionInfo.getTotalRepetitions());
}
name
에 {displayName}
, {currentRepetition}
, {totalRepetitions}
에 변수를 이용해서 이름을 이쁘게 꾸밀 수 있다.
파라미터를 테스트 반복
@ParameterizedTest
을 이용해서 값을 바꿔가며 테스트 할 수 있다.
단순하게는..
@ParameterizedTest
@ValueSource(strings = {"더위", "추위", "선선", "비"})
void parameterTest(String weather) {
System.out.println("날씨: " + value);
}
테스트의 이름을 꾸미고 싶다면 다음과 같이 에노테이션을 바꾸면 된다.
@DisplayName("값 바꾸며 반복")
@ParameterizedTest(name = "{displayName} {index}...{0}, {1}")
@ValueSource
말고도 다양한 에노테이션이 있다.
- @NullSource, @EmptySource, @NullAndEmptySource
- @EnumSource
- @MethodSource
- @CvsSource
- @CvsFileSource
- @ArgumentSource 이런 값 에노테이션에는 암묵적인 타입 변환이 있다.
테스트 인스턴스
기본적인 테스트 클래스의 인스턴스는 각 테스트 메소드마다 만들어지게 된다. (메소드가 100개면 100개..)
다음과 같은 코드를 실행하면 commonValue
의 값이 2개 나와야 할거 같지만..
public class ExampleTest {
int commonValue = 0;
@Test
void test1() {
commonValue++;
System.out.println(this + ".test1 : " + commonValue);
}
@Test
void test2() {
commonValue++;
System.out.println(this + ".test2 : " + commonValue);
}
}
결과는 1이 나온다.
me.sangoh.testing.ExampleTest@1c1bbc4e.test1 : 1
me.sangoh.testing.ExampleTest@247bddad.test2 : 1
왜 이런식으로 기본 동작이 각 메소드마다 인스턴스를 만드냐면 테스트 메소드를 독립적으로 실행하여 테스트 메소드간에 의존성을 제거하기 위함 이다.
테스트 인스턴스 공유하기
@TestInstance(Lifecycle.PER_CLASS)
에노테이션을 Class에 붙이면 테스트 메소드별로 생성하지 않고 하나의 인스턴스를 공유한다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class ExampleTest {
int commonValue = 0;
@Test
void test1() {
commonValue++;
System.out.println(this + ".test1 : " + commonValue);
}
@Test
void test2() {
commonValue++;
System.out.println(this + ".test2 : " + commonValue);
}
}
결과
me.sangoh.testing.ExampleTest@45c7e403.test1 : 1
me.sangoh.testing.ExampleTest@45c7e403.test2 : 2
테스트 순서
기본적으로 테스트 메소드의 실행 순서는 어떤 메소드가 먼저 실행될지 알 수 없다.
물론 Junit5부터는 메소드의 순서대로 작동이 되지만 하나의 메소드가 하나의 유닛테스트라면 테스트 순서에 의존해서도 신경을 써도 안된다
하지만… 그럼에도 테스트 메소드 실행 순서를 정하는 방법은 있다.
그러려면 @TestInstance(TestInstance.Lifecycle.PER_CLASS)
을 사용해서 인스턴스를 공유하게 하고
@TestMethodOrder
을 순서 정하는 방법을 정하고 순서를 정의하면 된다.
- Alphanumeric
- OrderAnnoation
- Random
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class ExampleTest {
int commonValue = 0;
@Order(1)
@Test
void test1() {
commonValue++;
System.out.println(this + ".test1 : " + commonValue);
}
@Order(2)
@Test
void test2() {
commonValue++;
System.out.println(this + ".test2 : " + commonValue);
}
}
JUnit 설정 파일
클래스패스 루트 (src/test/resources/)에 junit-platform.properties
을 넣으면 설정파일로 인식한다.
안된다고 하면… 다음 이미지를 보고 Project Test Resources
를 설정하자..
resource 설정
다음과 같은 것들을 설정 할 수 있다.
-
테스트 인스턴스 라이프사이클 설정
junit.jupiter.testinstance.lifecycle.default = per_class
-
확장팩 자동 감지 기능
junit.jupiter.extensions.autodetection.enabled = true
기본 값은 false -
@Disabled 무시하고 실행하기
junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition
-
테스트 이름 표기 전략 설정
junit.jupiter.displayname.generator.default = \ org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
JUnit 5 확장 모델
JUnit 4의 확장 모델은 @RunWith(Runner), TestRule, MethodRule.
JUnit 5의 확장 모델은 단 하나, Extension.
테스트에 확장팩을 등록 방법은
- 선언적인 등록 @ExtendWith
- 프로그래밍 등록 @RegisterExtension
- 자동 등록 자바 ServiceLoader 이용
확장팩을 만들면 할 수 있는 것들
- 테스트 실행 조건
- 테스트 인스턴스 팩토리
- 테스트 인스턴스 후-처리기
- 테스트 매개변수 리졸버
- 테스트 라이프사이클 콜백
- 예외 처리
테스트가 느린 메소드 찾는 확장팩
public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final String START_TIME = "START_TIME";
@Override
public void beforeTestExecution(ExtensionContext extensionContext) throws Exception {
String testClassName = extensionContext.getRequiredTestClass().getName();
String testMethodName = extensionContext.getRequiredTestMethod().getName();
ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.create(testClassName + "." + testMethodName));
store.put("START_TIME", System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext extensionContext) throws Exception {
String testClassName = extensionContext.getRequiredTestClass().getName();
String testMethodName = extensionContext.getRequiredTestMethod().getName();
ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.create(testClassName + "." + testMethodName));
Long startTime = store.remove(START_TIME, long.class);
long duration = System.currentTimeMillis() - startTime;
if (duration > 1000L) {
System.out.printf("Pleas check method [%s] this method is vary slow\n", testMethodName);
}
}
}
선언적으로 사용하기
@ExtendWith(FindSlowTestExtension.class)
class StudyTest {
혹은 프로그래밍적으로 추가하기
class StudyTest {
@RegisterExtension
static FindSlowTestExtension findSlowTestExtension = new FindSlowTestExtension();
...test...
}
JUnit3,4를 JUnit5로 마이그래이션
JUnit 3과 4 작성된 테스트 JUnit5의 junit-platform
을 사용해서 실행할 수 있다.
단 기본 기능이 아님으로 junit-jupiter-migrationsupport
의존성을 추가 해야한다.
자세한 내용은 Pass..