Search
Duplicate

JUnit5

목차

JUnit 소개

자바 개발자가 가장 많이 사용하는 테스팅 프레임워크
Java 8 이상이 필요하다.
대체제로 TestNg, Spock등 많지만 JUnit만 알아도 문제 없다.

JUnit 4와의 차이점

JUnit4이 하나의 Jar파일로 Dependency가 추가되어 JUnit이 참조하는 다른 라이브러리가 있는 형태
JUnit5부터는 그자체로 여러 모듈화가 되어있다.
1.
Platform: 테스트를 실행해주는 런처를 제공해준다. TestEngine API 제공
2.
Jupiter: TestEngine API 구현체로 JUnit 5를 제공한다.
3.
Vintage: JUnit4와 3을 지원하는 TestEngine 구현체이다.

JUnit 5 시작하기

1. 프로젝트 생성

Project: Maven Project
Language: Java
Spring Boot: 2.4.X
Project Metadata
Group: me.catsbi
Artifact: inflearn-the-java-study
Name: item-service
Package name: me.catsbi
packaging: Jar
Java: 11 +
참고: 2.2+@ 버전 스프링 부트는 기본적으로 JUnit5 의존성이 추가된다.

2. 기본적인 클래스와 테스트클래스 생성

src/main/java/me/catsbi/inflearnthejavatest/Study.java
public class Study {}
Java
복사
src/test/java/me/catsbi/inflearnthejavatest/StudyTest.java
package me.catsbi.inflearnthejavatest; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class StudyTest {}
Java
복사
JUnit5부터는 클래스나 메서드에 public 접근제어자를 붙히지 않아도 된다.
⇒ 아마도 Reflection API를 사용하기 때문.

참고

스프링 부트의경우 JUnit5 라이브러리가 자동 추가되어 있지만 스프링 부트가 아니여서 해당 라이브러리가 없을경우 설정부분 dependency에 다음 내용을 추가한다.
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.5.2</version> <scope>test</scope> </dependency>
XML
복사

3. JUnit5가 제공하는 기본 애노테이션 사용해보기

package me.catsbi.inflearnthejavatest; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; class StudyTest { @Test void create() { Study study = new Study(); assertNotNull(study); System.out.println("create"); } @Test void create1() { System.out.println("create1"); } @BeforeAll static void beforeAll() { 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"); } }
Java
복사
@Test
⇒ 해당 메소드를 테스트 대상으로 지정한다.
@BeforeAll
⇒ 모든 테스트시작 전에 수행되는 로직에 붙는 애노테이션으로 static을 붙혀줘야 하며 접근 제어자는 default 이상이여야 한다.
@AfterAll
⇒ 모든 테스트종료 후에 수행되는 로직에 붙는 애노테이션으로 static을 붙혀줘야 하며 접근 제어자는 default 이상이여야 한다.
@BeforeEach
⇒ 모든 @Test 애노테이션이 붙은 테스트 대상 메소드 수행 전마다 수행된다.
@AfterEach
⇒ 모든 @Test 애노테이션이 붙은 테스트 대상 메소드 수행 종료시마다 수행된다.
@Disabled
⇒ 해당 애노테이션이 붙으느 메서드는 테스트 제외 대상으로 테스트를 수행하지 않는다.

JUnit 5 테스트 이름 표시하기

1. @DisplayNameGeneration

⇒ 클래스에서도 사용할 수 있고 메서드에서도 사용이 가능한 애노테이션으로 속성으로 어떤 전략을 설정하느냐에따라서(strategy) 테스트 결과 출력결과의 이름을 바꿔줄 수 있다. 클래스에 애노테이션에 붙혀주면 클래스내부 모든 테스트 메서드에 적용이되고, 메서드에 적용하면 해당 메서드로 한정된다.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) public class TestClass { ... @Test void create_account_test(){ ... } }
Java
복사
위와같은 테스트 코드라면 출력결과는 애노테이션 속성으로 설정된 ReplaceUnderscores 대로 언더스코어가 공백으로 바뀌어서 create_account_test 의 출력된 부분이 create account test 로 출력될 것이다.

2. @DisplayName

어떤 테스트인지 테스트 이름을 보기 쉽게 표현할 수 있는 방법을 제공하는 애노테이션으로 우선순위는 @DisplayNameGeneration보다 높다. 그리고 이모지같은 이모티콘도 지원을 한다. 그리고 해당 애노테이션 역시 클래스와 메서드 모두 붙혀줄 수 있다.
@DisplayName("테스트 클래스") public class TestClass { ... @DisplayName("회원 생성 테스트") @Test void create_account_test(){ ... } }
Java
복사
위와같이 선언을 해놓으면 출력창에도 다음과 같이 출력된다.

JUnit 5 Assertion

실제 테스트에서 검증하고자 하는 내용을 확인하는 기능을 제공하는 패키지
테스트를 위해서 사전에 필요한 데이터세팅이나 준비를 위한 애노테이션과, 각각의 테스트에 대해서 출력을 어떻게 할지에 대한 전략선택까지 알아보았다. 그럼 이제 실제로 내가 구현한 기능을 테스트하기 위한 기능이 필요하다. 인자값 두개를 받아서 해당 두개의 인자값을 더해주는 덧셈메서드를 만들었다면 이 메서드의 반환값이 내가 생각한대로 나오는지 확인하기 위해서는 이를 검증해줄 기능이 필요한데, 이를 org.junit.jupiter.api.Assertions에서 제공해주는데 지원메서드는 다양하게 있는데 대표적으로다음과같은 메서드가 있다.
assertEquals(expected, actual)
: 실제 값(actual)이 기대한 값(expected)과 같은지 확인하는 메서드
@DisplayName("스터디 만들기 ") @Test void create_new_study() { Study study = new Study(); assertEquals(StudyStatus.DRAFT, study.getStatus(),()-> "스터디를 처음 만들면 DRAFT 상태다. "); }
Java
복사
→ assertEquals에서 세 번째 파라미터로 메세지를 줄 수 있는데 해당 테스트가 통과하지 못했을 경우 출력된다. 이때 이를 String 리터럴로 줄수도 있지만 Supplier<String> 타입의 인스턴스를 람다로 제공할수도 있는데, 복잡한 메세지를 생성해야 하는경우 람다를 사용하면 실패한 경우에만 해당 메세지를 만들 수 있어 효율적이다.
assertNotNull(actual)
: 결과 값(actual)이 null인지 아닌지 확인하는 메서드
@DisplayName("스터디 만들기 ") @Test void create_new_study() { Study study = new Study(); assertNotNull(study); }
Java
복사
assertTrue(boolean)
: 다음 조건이 참인지 확인하는 메서드
@DisplayName("스터디 만들기 ") @Test void create_new_study() { Study study = new Study(); assertTrue(study.getStatus().equals(StudyStatus.DRAFT)); }
Java
복사
assertAll(executable...)
: 모든 확인 구문 확인 메서드
@DisplayName("스터디 만들기 ") @Test void create_new_study() { Study study = new Study(); assertNotNull(study); assertAll( ()->assertEquals(StudyStatus.DRAFT, study.getStatus(),()-> "스터디를 처음 만들면 DRAFT 상태다. "), ()->assertTrue(study.getLimit() > 0, ()-> "스터디 최대 참석 가능 인원은 0명 이상이어야 한다. ") ); }
Java
복사
assertThrows(expectedType, executable)
: 예외 발생 확인 메서드
@DisplayName("스터디 만들기 ") @Test void create_new_study() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Study(-1)); assertEquals(exception.getMessage(), "limit 은 0보다 커야한다."); }
Java
복사
→ 예외 검증 후 해당 예외를 반환해주기에 예외내용중 메세지나 기타 검증할 필요가있을때 검증이 가능하다.
assertTimeout(duration, executable)
:특정시간 내에 실행이 완료되는지 확인하는 메서드
@DisplayName("스터디 만들기 ") @Test void create_new_study() { assertTimeout(Duration.ofMillis(100), () -> { new Study(10); Thread.sleep(300); }); }
Java
복사
⇒ sleep 메서드를 통해 300 ms 대기하도록 하기에 해당 테스트는 실패를 한다.
참고: 만약 300ms를 다 기다리지 않고 해당 대기시간(100ms)가 지나 테스트가 실패하자마자 테스트가 중단되도록하려면 assertTimeoutPreemtively()메서드를 사용하면 된다. 하지만, 스프링 트랜잭션이 제대로 동작을 안할수 있어서 롤백기반인 스프링 테스트에서 롤백이 안된다던가 하는 부작용이 있기에 사용을 권장하지는 않는다. ThreadLocal 같은 쓰레드와 전혀 관련히 없는 코드를 실행할때는 사용해도 괜찮다.

조건에 따라 테스트 실행하기

JUnit에서는 특정한 조건에따라 테스트를 실행할수도 있다.
ex) 특정한 OS, 환경변수, 시스템변수에 따라 테스트를 실행해야할지 말지를 결정해야하는경우

org.junit.jupiter.api.Assumptions.*

이 패키지에서 제공해주는 다음 메서드를 사용해서 조건을 적용할 수 있다.
assumeTrue(condition)
: 해당 조건이 통과를 해야 아래 로직들이 수행된다.
assumingThat(condition, test)
:조건(condition)이 통과를하면 두번째 파라미터로 전달한 로직이 수행된다.
@DisplayName("스터디 만들기 ") @Test void create_new_study() { String test_evn = System.getenv("TEST_ENV"); System.out.println(test_evn); assumeTrue("LOCAL".equalsIgnoreCase(test_evn)); assumingThat("LOCAL".equalsIgnoreCase(test_evn), () -> { System.out.println("local"); Study actual = new Study(100); assertTrue(actual.getLimit() >= 0); }); assumingThat("catsbi".equalsIgnoreCase(test_evn), () -> { System.out.println("catsbi"); Study actual = new Study(10); assertTrue(actual.getLimit() >= 0); }); }
Java
복사

애노테이션으로 조건 설정

@EnabledOnOs
:특정 OS일 때만 테스트가 동작하게 할 수도 있다. (@EnabledOnOs(OS.MAC))
@EnabledOnJre
:특정 JRE 버전일때만 테스트가 동작하게 할 수도 있다.
(@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9, JRE.JAVA_10, JRE.JAVA_11}))
@EnabledIfEnvironmentVariable
:위에서 사용한 assumeXX 메서드는 해당 애노테이션을 통해 환경변수 조건 설정이 가능하다.
(@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "local"))

태깅과 필터링

우리가 작성한 테스트가 여러개 있는데, 이를 그룹화 할 수 있다(ex: 모듈별, 단위/통합 구분, 기타 조건)
@Tag
: 테스트 메서드에 추가할 수 있는 태그 애노테이션으로 하나에 테스트메서드에 여러 태그를 사용할수도있다.
package me.catsbi.inflearnthejavastudy; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; class StudyTest { @DisplayName("스터디 만들기 fast") @Test @Tag("fast") void create_new_study() { Study study = new Study(); assertEquals(StudyStatus.DRAFT, study.getStatus(),()-> "스터디를 처음 만들면 DRAFT 상태다. "); } @DisplayName("스터디 만들기 slow") @Test @Tag("slow") void create_new_study_again() { Study study = new Study(); assertEquals(StudyStatus.DRAFT, study.getStatus(),()-> "스터디를 처음 만들면 DRAFT 상태다. "); } }
Java
복사
스터디 객체를 생성하는 테스트 두개를 각각 slow, fast 태그로 구분을 했다.
하지만, 실행을 해보면 둘 다 수행되는 것을 볼수 있다. 이는 아직 태그는 붙혔지만 필터링을 해주지 않았기 때문이다.

필터링

IntelliJ 에서 특정 태그로 테스트 필터링
→ Run/Debug Configurations 에서 Tags 항목으로 선택 후 실행할 테스트의 태그명을 적어주면 된다.
Maven - pom.xml 작성을 통한 필터링
1.
pom.xml 작성
<profiles> <profile> <id>default</id> <activation> <activeByDefault>true</activeByDefault> </activation> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <configuration> <groups>fast</groups> </configuration> </plugin> </plugins> </build> </profile> </profiles>
XML
복사
→ 만약 slow도 테스트를 하고싶다면 or 문법인 | 연산자를 이용해서 작성을 해주면 된다. <groups>fast | slow</groups>
2.
테스트 실행해보기

커스텀 태그

JUnit 에서 제공하는 테스트용 애노테이션은 메타 애노테이션으로 사용할 수 있다.
즉, 자주사용하는 애노테이션의 조합을 하나의 ComposedAnnotation으로 만들어서 편리하게 사용할 수 있다.
Fast라는 임의의 애노테이션을 만들어서 메타애노테이션을 정의해보자.

코드

FastTest
package me.catsbi.inflearnthejavastudy; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Test @Tag("fast") public @interface FastTest { }
Java
복사
SlowTest
package me.catsbi.inflearnthejavastudy; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Test @Tag("slow") public @interface SlowTest { }
Java
복사
StudyTest
package me.catsbi.inflearnthejavastudy; import org.junit.jupiter.api.DisplayName; import static org.junit.jupiter.api.Assertions.assertEquals; class StudyTest { @DisplayName("스터디 만들기 fast") @FastTest void create_new_study() { Study study = new Study(); assertEquals(StudyStatus.DRAFT, study.getStatus(),()-> "스터디를 처음 만들면 DRAFT 상태다. "); } @DisplayName("스터디 만들기 slow") @SlowTest void create_new_study_again() { Study study = new Study(); assertEquals(StudyStatus.DRAFT, study.getStatus(),()-> "스터디를 처음 만들면 DRAFT 상태다. "); } }
Java
복사
→ 기존에 작성되있던 @Test, @Tag("fast or slow") 애노테이션이 사라지고 임의로 작성한 애노테이션(@FastTest, @SlowTest)가 붙어있다.
→ 매번 Tag를 작성하여 속성을 직접 입력하면 오타가 날 확률이 있는데 메타 애노테이션으로 이를 해결한다.

테스트 반복하기

인자가 랜덤값이거나, 테스트 발생시점에 따라 달라지는 값 때문에, 테스트내용이 반복되야하는 경우에는 어떻게 해야할까? 단순히 for, while문을 사용해야할까? JUnit에서는 이러한 테스트 반복을 위한 애노테이션을 제공한다.
@RepeatedTest
:속성을 통해 반복 횟수와 반복 테스트 이름을 설정할수 있다.
@DisplayName("반복테스트") @RepeatedTest(value = 10, name = "{displayName} {currentRepetition}/{totalRepetitions}") void repeatTest(RepetitionInfo repetitionInfo) { System.out.println("repetitionInfo.getCurrentRepetition() = " + repetitionInfo.getCurrentRepetition()); System.out.println("repetitionInfo.getTotalRepetitions() = " + repetitionInfo.getTotalRepetitions()); }
Java
복사
→ RepetitionInfo 타입의 인자를 받을 수 있다. 해당 인자에는 반복에 대한 정보를 얻을 수 있다.
→ 애노테이션의 name속성에 테스트 이름, 반복 횟수등을 설정할 수 있다.
{displayName} : @DisplayName으로 설정한 테스트 이름
{currentRepetition}: 현재 반복 횟수 값
{totalRepetition}: 전체 반복 횟수
@ParameterizedTest
: 테스트에 여러 다른 매개변수를 대입해가며 반복 실행한다.
@ParameterizedTest(name = "{index} {displayName} 과목:{0}") @ValueSource(strings = {"수학", "영어", "국어", "체육"}) void create_new_study(String input) { System.out.println("input = " + input); }
Java
복사
@ValueSource
→ 속성에 선언해준 파라미터를 인자값으로 전달해주는데 string 타입 뿐 아니라 각 원시타입을 모두 지원한다.
@ParameterizedTest(name = "{index} {displayName} 과목:{0}")
→ {displayName} : @DisplayName 애노테이션으로 설정한 테스트 이름
→ {index} 현재 반복된 횟수의 인덱스
→ {arguments} : 전달되는 인덱스 전체
→ {0}, {1}...: 파라미터를 인덱스로 조회할 수 있다.

인자값들의 소스

위에서 간단하게 @ValueSource에 대해 알아봤는데 이렇게 인자 값들을 제공해주는 애노테이션은 그외에 더 있다.
@ValueSource
: 다양한 기본타입의 인자값을 전달해줄 수 있다.
@NullSource, @EmptySource, @NullAndEmptySource
: Null과 공백문자를 제공해주는 애노테이션으로 각각 제공해줄수도 있고 @NullAndEmptySource를 통해 둘 다 제공해줄 수도 있다.
@EnumSource
:Enum의 값을 제공해주는 애노테이션
@DisplayName("스터디 만들기") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @EnumSource(value = StudyStatus.class) void create_new_study_again_again(StudyStatus status) { System.out.println("status = " + status); } //status = DRAFT //status = COMPLETED
Java
복사
@MethodSource
:특정 메서드를 만들어서 인자값을 전달받는다.
@DisplayName("스터디 만들기") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @MethodSource("provideNamesAndLimit") void create_new_study_again_again(String name, int input) { System.out.println("name:" + name + ", limit:" + input); } private static Stream<Arguments> provideNamesAndLimit() { return Stream.of( Arguments.of("수학", 10), Arguments.of("영어", 20), Arguments.of("국어", 30) ); }
Java
복사
@CvsSource
:delimiter를 통해 속성의 value로 세팅한 값을 구분해서 인자로 전달한다.
@DisplayName("스터디 만들기") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @CsvSource(value = {"수학:1", "영어:2", "국어:3", "체육:4"}, delimiter = ':') void create_new_study_again(String input, int limit) { System.out.println("input = " + input); }
Java
복사
@CvsFileSource
:파일을 읽어와서 인자로 제공하는 애노테이션
@ArgumentSource
:ArugmentProvider의 구현체 클래스로부터 인자값을 전달하는 애노테이션
@DisplayName("스터디 만들기") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @ArgumentsSource(value = TestArgumentProvider.class) void create_new_study_again(String input) { System.out.println("input = " + input); } static class TestArgumentProvider implements ArgumentsProvider{ @Override public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) throws Exception { return Stream.of( Arguments.of("수학", 10), Arguments.of("영어", 20), Arguments.of("국어", 30) ); } }
Java
복사

인자 값 타입 변환

내가 애노테이션의 속성으로 String을 값을 전달하더라도 실제로는 다른 타입으로 쓰고싶을 수 있다.
그래서 JUnit에서는 암묵적인 변환 방법과 명시적인 변환방법을 모두 제공한다.

암묵적인 타입 변환

개발자가 따로 명시하지 않아도 코드 컨벤션에 따라 자동으로 변환이 될 수 있는데 예를들어 "true"라는 문자열을 제공해줄때 매개변수를 boolean value로 받는다면 boolean 타입의 true 값으로 대입된다.
그리고 그 밖에도 많은 데이터들에 대해서 암묵적으로 타입변환을 제공하는데 다음 표를 참고하면 된다.
더 자세한 내용은 다음 레퍼런스(링크) 를 참고하면 된다.

명시적인 타입 변환

그럼 내가 직접만든 객체(커스텀한 타입)로 변환을 하려면 어떻게 해야할까?
그 경우에는 여러 방식이 있는데 하나씩 알아본다.
SimpleArgumentConverter 을 구현해서 적용
:보통 하나의 인자값을 변환하고자 할 때 사용하며 static inner class 이거나 다른 public class의 static 클래스여야 한다.
@DisplayName("스터디 만들기") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @ValueSource(ints = {1,2,3,4,5}) void create_new_study_again(@ConvertWith(StudyConverter.class)Study study) { System.out.println("study = " + study.toString()); } static class StudyConverter extends SimpleArgumentConverter{ @Override protected Object convert(Object o, Class<?> aClass) throws ArgumentConversionException { assertEquals(Study.class, aClass, "Can Only convert to Study"); return new Study(Integer.parseInt(o.toString())); } }
Java
복사
→ 매개변수를 전달받는 메서드에서는 내가 변환받고자 하는 매개변수에 @ConvertWith 애노테이션을 붙혀서 사용한다.
ArgumentsAccessor을 매개변수로 받아 사용
: 하나 이상의 인자값을 받고 싶을 때 해당 매개변수를 받아 사용할 수 있다.
@DisplayName("스터디 만들기") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @CsvSource({"10,'수학'", "20,스프링"}) void create_new_study_again_with_csvsource(ArgumentsAccessor accessor) { System.out.println(accessor.getInteger(0)); System.out.println(accessor.getString(1)); }
Java
복사
→ 매개변수로 받은 accessor를 이용해 인덱스를 활용한 getter로 값을 꺼내 사용할 수 있다.
ArgumentsAggregator를 사용해 커스텀 타입 변환
:accessor를 사용하지 않고 해당 인터페이스를 구현해 객체를 사전에 생성해서 전달할수도 있다.
@DisplayName("스터디 만들기") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @CsvSource({"10,'수학'", "20,스프링"}) void create_new_study_again_with_aggregator(@AggregateWith(StudyAggregator.class) Study study) { System.out.println("study = " + study); } static class StudyAggregator implements ArgumentsAggregator { @Override public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext parameterContext) throws ArgumentsAggregationException { return new Study(accessor.getString(1), accessor.getInteger(0)); } }
Java
복사
→ 코드 내부적으로는 accessor를 이용해 객체를 생성을 Aggregator에서 해주는 것 뿐이다.
→ 매개변수로 받을 부분에서 @AggregateWith 애노테이션을 사용해 해당 Aggregator를 사용한다.

테스트 인스턴스

JUnit은 테스트 메소드별로 테스트 인스턴스를 새로 만들며 이것이 기본전략이다.
이처럼 각각의 메소드를 독립적으로 실행할 경우 다른 메소드로부터 영향을 받지 않기에 예상치 못한 다른 테스트로부터의 영향을 받을일이 줄어든다. 하지만, JUnit5 에서는 상황에따라 이러한 전략을 바꿔줄수도 있다.

@TestInstance(Lifecycle.PER_CLASS)

테스트를 클래스당 인스턴스를 하나만 만들어서 사용한다.
경우에 따라서 테스트 간에 공유하는 모든 상태를 @BeforeEach 또는 @AfterEach에서 초기화 할 필요가 있다.
@BeforeAll과 @AfterAll을 인스턴스 메소드 또는 인터페이스에 정의한 default메소드로 정의할 수 있다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS) class StudyTest { @BeforeAll void beforeAll() { System.out.println("beforeAll"); } @AfterAll void afterAll() { System.out.println("afterAll"); } ... }
Java
복사
@BeforeAll, @AfterAll 애노테이션은 테스트 인스턴스가 PER_CLASS로 클래스당 하나의 인스턴스만 생성하도록 되면 static 키워드를 제거하여 인스턴스 메서드로 선언할 수 있다.

테스트 순서

테스트 메서드는 특정한 순서에 의해 실행이 되지만 그 순서가 어떻게 정해지는지에 대해서는 의도적으로 밝히고 있지 않는다. 그 이유는 하나의 단위(테스트)는 독립적으로 실행되며 다른 메서드에 영향을 주면안된다. 그렇기에 순서에 의존하면 안되기 때문이다.
하지만, 특정 순서대로 테스트를 실행해야 하는 경우(Functional Test, Acceptance Test...)가 있다면 애노테이션을 통해 이 테스트 메서드의 실행순서를 제어할 수 있다.

@MethodOrderer

기본 구현체
→ MethodOrderer.OrderAnnotation.class
→ MethodOrderer.Alphanumeric.class
→MethodOrderer.Random.class
참고: @TestInstance(Lifecycle.PER_CLASS)와 함께 사용을 하지 않아도 된다.
테스트 순서 애노테이션들은 작동을 한다. 다만, 해당 애노테이션으로 클래스단위로 테스트가 수행된다면, 상태가 공유되는 stateful 한 테스트를 클래스단위로 실행할때 유용하다.

코드

@TestInstance(TestInstance.Lifecycle.PER_CLASS) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class StudyTest { @Order(0) @DisplayName("반복테스트") @RepeatedTest(value = 10, name = "{displayName} {currentRepetition}/{totalRepetitions}") void repeatTest(RepetitionInfo repetitionInfo) { System.out.println(this); System.out.println("repetitionInfo.getCurrentRepetition() = " + repetitionInfo.getCurrentRepetition()); System.out.println("repetitionInfo.getTotalRepetitions() = " + repetitionInfo.getTotalRepetitions()); } @Order(1) @DisplayName("스터디 만들기 1") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @ValueSource(strings = {"수학", "영어", "국어", "체육"}) void create_new_study(String input) { System.out.println(this); System.out.println("input = " + input); } @Order(2) @DisplayName("스터디 만들기 2") @ParameterizedTest(name = "{index} {displayName} 과목:{arguments}") @ValueSource(ints = {1,2,3,4,5}) void create_new_study_again(@ConvertWith(StudyConverter.class)Study study) { System.out.println(this); System.out.println("study = " + study.toString()); } }
Java
복사
→ OrderAnnotation기준으로 실행이 된다.
→ 각각의 테스트 메서드들이 @Order가 있는데 안에 작성한 value값에따라 순서대로 수행된다
→ 만약 @Order내부의 값이 같을 경우 자체적인 순서에따라 수행된다.

junit-platform.properties

JUnit은 설정파일(properties)을 이용해 클래스 패스 루트(src/test/resources/)에 넣어두면 적용된다.
#테스트 인스턴스 라이프 사이클 설정 # 테스트 인스턴스 라이프사이클 설정 junit.jupiter.testinstance.lifecycle-default=per_class # 확장팩 자동 감지 기능 junit.jupiter.extensions.autodetection.enabled=true # @Disabled 무시하고 실행하기 junit.jupiter.conditions.deactivate=org.junit.*DisabledCondition # 테스트 이름 표기 전략 설정 junit.jupiter.displayname.generator.default=org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
YAML
복사
이런 설정파일을 만들어서 설정을 하는건 어떤 장점이 있을까? 테스트가 한 두개정도일때는 기존에 하던대로 애노테이션 선언만으로도 충분하다.
하지만, 테스트 클래스가 수십 수백개가 된다면 이를 모두 하나하나 애노테이션을 붙혀주고 작업을 해야할까? 이런 경우 외부 설정파일을 통해 통합해서 관리를 해줄 수 있다.
E

확장 모델

JUnit5JUnit4에 비해서 간결해졌는데, 기존 JUnit4 에서는 @RunWith(Runner), TestRule, MethodRule을 사용하는 방법으로 확장 방법이 나눠져있었는데, JUnit5에서 Extension으로 하나로 통합되었다.
참고: 공식 Document
무엇을 확장할 수 있는지에 대해서 문서를 살펴보면 정말 많은 부분을 확장할 수 있다.
등록하는 방법, 테스트 실행여부, 테스트 인스턴스, 테스트 인스턴스를 만든 후, 파라미터 리졸빙방법등 많은 확장 방법을 제공한다.

확장팩 등록 방법

1. 선언적인 등록 @ExtendWith

FindSlowTestExtension
테스트의 수행시간을 체크 후 THRESHOLD(1초)보다 오래걸리면 @SlowTest 애노테이션을 붙히라고 출력하는 확장 모델
package me.catsbi.inflearnthejavatest; import org.junit.jupiter.api.extension.AfterTestExecutionCallback; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; import org.junit.jupiter.api.extension.ExtensionContext; import java.lang.reflect.Method; public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { private static final long THRESHOLD = 1000L; @Override public void afterTestExecution(ExtensionContext context) throws Exception { String testMethodName = context.getRequiredTestMethod().getName(); ExtensionContext.Store store = getStore(context); Long startTime = store.remove("START_TIME", long.class); long duration = System.currentTimeMillis() - startTime; if (duration > THRESHOLD) { System.out.printf("Please consider mark method [%s] with @SlowTest. \n", testMethodName); } } @Override public void beforeTestExecution(ExtensionContext context) throws Exception { ExtensionContext.Store store = getStore(context); store.put("START_TIME", System.currentTimeMillis()); } private ExtensionContext.Store getStore(ExtensionContext context) { String testClassName = context.getRequiredTestClass().getName(); Method testMethodName = context.getRequiredTestMethod(); return context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName)); } }
Java
복사
위 확장 모델을 내가 사용하고자 하는 테스트 클래스에 클래스 레벨에 붙혀준다.
StudyTest
@DisplayName("스터디 도메인 테스트") @ExtendWith(FindSlowTestExtension.class) class StudyTest { @Test @DisplayName("스터디 만들기") void create() throws InterruptedException { Study study = new Study(); assertNotNull(study); Thread.sleep(1005L); System.out.println("create"); } }
Java
복사
@ExtendWith(FindSlowTestExtension.class) 클래스 레벨에 확장 애노테이션을 붙혀준다.
⇒ 이를 선언적인 등록이라고 한다.
⇒ 테스트 메서드는 출력 확인을 위해임의로 1005ms 간 대기하도록 했다.
⇒ [실행 결과]
참고: 이미 @SlowTest 애노테이션을 붙혔다면 확장 모델 코드를 수정해 애노테이션이 이미 있는경우에는 출력이 되지 않도록 변경해주자.
@Override public void afterTestExecution(ExtensionContext context) throws Exception { Method requiredTestMethod = context.getRequiredTestMethod(); SlowTest annotation = requiredTestMethod.getAnnotation(SlowTest.class); String testMethodName = context.getRequiredTestMethod().getName(); ExtensionContext.Store store = getStore(context); Long startTime = store.remove("START_TIME", long.class); long duration = System.currentTimeMillis() - startTime; if (duration > THRESHOLD && Objects.isNull(annotation)) { System.out.printf("Please consider mark method [%s] with @SlowTest. \n", testMethodName); } }
Java
복사
⇒ requiredTestMethod.getAnnotation(SlowTest.class)
: 테스트 메서드에 붙어있는 애노테이션을 Reflection API를 통해 가져오는데 이중 SlowTest.class 애노테이션을 가져오도록 한다.
⇒ Objects.isNull(annotation)
:조건문에 해당 애노테이션이 없는 경우(null인경우)에만 해당 메세지가 출력되도록 했다.
즉, 이제는 테스트 수행시간이 1000ms보다 크면서 @SlowTest애노테이션이 없는경우 메세지가 출력되는 확장 모델이 완성되었다.

2. 프로그래밍 등록 @RegisterExtension

: 애노테이션으로 등록을 하면(선언적인 방법) 커스터마이징이 불가능하다. 내가 테스트 할때마다 THRESHOLD를 다르게 주고싶어도, 가능하지가 않다.
그래서 테스트 클래스의 필드에 직접 선언을해서 사용을 해줘야 한다.
StudyTest
@DisplayName("스터디 도메인 테스트") class StudyTest { @RegisterExtension static FindSlowTestExtension findSlowTestExtension = new FindSlowTestExtension(1000L); ... }
Java
복사
⇒ 해당 클래스에서 사용할 확장모델을 생성하며 THRESHOLD값을 생성자 인자값으로 전달한다.

다음 챕터로