JUnit 5 소개
자바 개발자가 가장 많이 사용하는 테스팅 프레임워크
자바 8 이상이 필요하다
Platform
테스트를 실행해주는 런처를 제공한다.
TestEngine API 제공
우리가 인텔리제이에서 메인 메소드 없이 테스트를 진행할 수 있는 이유는 인텔리제이가 JUnit 플랫폼과의 통합 테스팅 환경을 제공하기 때문이다.
IDE가 내부적으로 JUnit Platform 런처를 사용해 테스트를 자동으로 실행한다.
Jupiter
TestEngine 구현체 Junit5 제공
Vintage
Junit 4,3 지원하는 TestEngine 구현체
JUnit 5 시작하기
2.2+ 버전의 스프링 부트 프로젝트를 만든다면 기본으로 JUnit 5 의존성 추가 된다
build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
기본 애노테이션
@Test
테스트 메소드를 표시하는 애노테이션
메인 로직의 기능을 검증하기 위해 작성된 메소드에 사용
기본적으로 예외가 발생하거나 Assertions로 검증에 실패하면 테스트는 실패한다.
@BeforeAll / @AfterAll
@BeforeAll: 모든 테스트가 실행되기 전에 한 번만 실행되는 메소드를 정의
@AfterAll: 모든 테스트가 실행된 후 한 번만 실행되는 메소드를 정의
주로 리소스 초기화 및 정리에 사용된다
주의: static 메소드여야 한다.
@BeforeEach / @AfterEach
@BeforeEach: 각 테스트가 실행되기 전에 실행되는 메소드를 정의
@AfterEach: 각 테스트가 실행된 후에 실행되는 메소드를 정의
각 테스트의 초기 상태를 설정하거나 테스트 후 정리를 수행하는 데 유용
@Disabled
특정 테스트를 비활성화(실행하지 않음)하고 싶을 때 사용한다.
테스트가 아직 구현되지 않았거나, 임시로 테스트를 중단해야 하는 경우 유용
예제 코드
class StudyTest {
@Test
void create1(){
Study study = new Study();
assertNotNull(study);
System.out.println("create1");
}
@Test
@Disabled
void create2(){
System.out.println("create2");
}
@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");
}
}
실행 결과
before all
before each
create1
after each
after all
JUnit 5: 테스트 이름 표시하기
사용 X
class DisplayTest {
@Test
void display_test1(){
System.out.println("test1");
}
@Test
void display_test2(){
System.out.println("test2");
}
}
실행을 시키면 테스트 이름이 이렇게 메소드 이름을 기반으로 만들어진다.
@DisplayNameGeneration
Method, Class 래퍼런스를 사용해서 테스트 이름을 표기하는 방법을 설정
기본 구현체로 ReplaceUnderscores 제공
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class DisplayTest {
@Test
void display_test1(){
System.out.println("test1");
}
@Test
void display_test2(){
System.out.println("test2");
}
}
@DisplayName
어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 애노테이션
@DisplayNameGeneration 보다 우선 순위가 높다
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class DisplayTest {
@Test
@DisplayName("테스트 \uD83D\uDE31")
void display_test1(){
System.out.println("test1");
}
@Test
void display_test2(){
System.out.println("test2");
}
}
이렇게 이모지를 넣을 수도 있다.
참고
https://junit.org/junit5/docs/current/user-guide/#writing-tests-display-names
JUnit5 Assertion
org.junit.jupiter.api.Assertions.*
assertEquals(expected, actual) | 실제 값이 기대하는 값과 같은지 확인 |
assertNotNull(actual) | 값이 null이 아닌지 확인 |
assertTrue(boolean) | 다음 조건이 참인지 확인 |
assertThrows(expectedType, executable) | 예외 발생 확인 |
assertTimeout(duration, executable) | 특정 시간 안에 실행이 완료되는지 확인 |
assertAll(executables...) | 모든 구문 확인 |
StudyStatus
public enum StudyStatus {
DRAFT, STARTED, ENDED
}
Study
public class Study {
private StudyStatus status;
public Study() {
}
public StudyStatus getStatus() {
return status;
}
}
StudyTest
class StudyTest {
@Test
@DisplayName("스터디 만들기")
void create_new_study(){
Study study = new Study();
assertNotNull(study);
assertEquals(StudyStatus.DRAFT, study.getStatus(), "스터디를 처음 만들면 상태값이 DRAFT 여야 한다");
}
}
실행 결과
assertEquals
public static void assertEquals(Object expected, Object actual) {
AssertEquals.assertEquals(expected, actual);
}
public static void assertEquals(Object expected, Object actual, String message) {
AssertEquals.assertEquals(expected, actual, message);
}
public static void assertEquals(Object expected, Object actual, Supplier<String> messageSupplier) {
AssertEquals.assertEquals(expected, actual, messageSupplier);
}
assertEquals 메소드는 이렇게 다양한 매개변수를 받을 수 있게 오버로딩 되어있다.
기대하는 값과 실제 값이 다를 때 메시지를 매개변수로 넘겨서 에러 메시지를 테스트하는 사람에게 더 명확하게 알려줄 수 있다.
만약 에러 메시지의 생성 비용이 크다면 Supplier를 넘겨서 최대한 생성 시점을 에러가 발생하여 필요로 하는 시점까지 늦출 수 있다.
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면 상태값이 DRAFT 여야 한다");
에러 메시지의 비용이 걱정이 된다고 하면 Supplier를 고려하자
assertAll
public class Study {
private StudyStatus status;
private int limit;
public Study() {
}
public StudyStatus getStatus() {
return status;
}
public int getLimit() {
return limit;
}
}
@Test
@DisplayName("스터디 만들기")
void create_new_study(){
Study study = new Study();
assertNotNull(study);
assertTrue(study.getLimit() > 0, "스터디 참석 가능 인원은 0보다 커야 한다");
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면 상태값이 DRAFT 여야 한다");
}
현재 테스트는 2곳에서 기대하는 값과 다르다.
1. limit은 0으로 초기화해서 0보더 커야 하는 조건을 못 맞춘다.
2. 스터디를 생성하면 상태 값이 DRAFT 여야 하는데 null 이다.
하지만 테스트를 돌리면 assertTrue 부분에서 멈추고 해당 부분의 내용만 테스트 결과로 나온다.
1개의 테스트 메소드에서 테스트 하는 부분 전체를 묶어서 테스트 하고 싶다면?
assertAll을 사용한다.
모든 테스트 검증(assertion)을 실행한 후 결과를 한꺼번에 보여준다
assertAll
public static void assertAll(Executable... executables) throws MultipleFailuresError {
AssertAll.assertAll(executables);
}
Executable
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface Executable {
void execute() throws Throwable;
}
assertAll 적용
class StudyTest {
@Test
@DisplayName("스터디 만들기")
void create_new_study(){
Study study = new Study();
assertAll(
() -> assertNotNull(study),
() -> assertTrue(study.getLimit() > 0, "스터디 참석 가능 인원은 0보다 커야 한다"),
() -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면 상태값이 DRAFT 여야 한다")
);
}
}
실행 결과
assertThrows
public class Study {
private StudyStatus status;
private int limit;
public Study() {
}
public Study(int limit) {
if(limit <= 0) throw new IllegalArgumentException("limit 0보다 커야 한다.");
}
public StudyStatus getStatus() {
return status;
}
public int getLimit() {
return limit;
}
}
Study 생성자에 매개변수인 limit가 0보다 작거나 같으면 IllegalArgumentException을 던진다.
테스트 실행
@Test
@DisplayName("스터디 만들기")
void create_new_study(){
IllegalArgumentException exception =
assertThrows(IllegalArgumentException.class, () -> new Study(-10));
assertEquals( "limit 0보다 커야 한다.", exception.getMessage());
}
assertThrows를 통해서 IllegalArgumentException 발생을 확인하고
assertEquals를 통해 예외 메시지까지 확인
assertThrows
public static <T extends Throwable> T assertThrows(Class<T> expectedType, Executable executable) {
return AssertThrows.assertThrows(expectedType, executable);
}
assertTimeOut
assertTimeout
public static void assertTimeout(Duration timeout, Executable executable) {
AssertTimeout.assertTimeout(timeout, executable);
}
마찬가지로 다양하게 오버로딩 되어있다. (메시지, 메시지(Supplier))
테스트 실행
@Test
void assert_time_out(){
assertTimeout(Duration.ofMillis(100), () -> {
Study study = new Study();
Thread.sleep(1000);
});
}
테스트를 보면 1초가 넘게 걸린 걸 확인할 수 있다.
이는 executable에 있는 로직이 끝나야만 테스트도 끝나는 것이다.
즉. executable에 있는 로직이 10초 걸리면 테스트는 10초 이상이 걸린다.
assertTimeoutPreemptively
@Test
void assert_time_out(){
assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
Study study = new Study();
Thread.sleep(1000);
});
}
마찬가지로 테스트는 실패하지만 assertTimeout과 다르게 지정한 Duration이 넘어가면 테스트는 바로 종료된다.
주의사항
assertTimeoutPreemptively에서 실행하는 스레드는 테스트를 실행하는 스레드와 다른 스레드이다.
스프링 테스트 환경에서는 보통 @Transactional이 기본적으로 테스트 메서드에 적용되어, 테스트 완료 후 자동으로 롤백된다. 기본이 롤백이지만 이 경우 별도의 스레드에서 실행되기 때문에 트랜잭션이 전파되지 않아 DB에 반영될 수 있다.
assertTimeout은 같은 스레드에서 실행되서 이런 케이스가 발생하지 않는다.
다른 라이브러리
AssertJ, Hemcrest, Truth 등의 라이브러리를 사용할 수도 있다.
assertThat, matcher 등등...
AssertJ, Hemcrest는 Spring Boot start를 사용하면 기본적으로 있다.
JUnit5 조건에 따라 테스트 실행하기
assumeTrue
환경변수 설정에 따라서 테스트를 실행하고 싶은 부분이 있고 아닌 부분이 있다.
class ConditionTest {
@Test
void test(){
String testEnv = System.getenv("TEST_ENV");
System.out.println(testEnv);
assumeTrue("LOCAL".equalsIgnoreCase(testEnv));
Study study = new Study(10);
assertThat(study.getLimit()).isGreaterThan(9);
}
}
assumeTrue: import static org.junit.jupiter.api.Assumptions.assumeTrue;
assertThat: import static org.assertj.core.api.Assertions.assertThat;
현재 TEST_ENV의 환경 변수는 없다.
그래서 출력도 null 이 나오고 assumeTrue 결과가 false이기 때문에 테스트는 더 진행되지 않고 끝난다.
환경 변수 추가
실행 결과
assumingThat
@Test @DisplayName("assumingThat")
void test2(){
String testEnv = System.getenv("TEST_ENV");
assumingThat("LOCAL".equalsIgnoreCase(testEnv), () -> {
System.out.println("LOCAL");
Study study = new Study(10);
assertThat(study.getLimit()).isGreaterThan(9);
});
assumingThat("TEST".equalsIgnoreCase(testEnv), () -> {
System.out.println("TEST");
Study study = new Study(10);
assertThat(study.getLimit()).isGreaterThan(9);
});
}
assumingThat: import static org.junit.jupiter.api.Assumptions.assumingThat;
실행 결과 (환경 변수 TEST_ENV=LOCAL)
환경 변수가 LOCAL이기 때문에 assumingThat에 해당하는 부분만 테스트가 진행된다.
@EnabledOnOS & @DisabledOnOS
테스트를 특정 운영 체제(OS)에서만 실행하거나 제외할 수 있게 해주는 조건부 테스트 애노테이션
class ConditionTest {
@Test
@EnabledOnOs({OS.MAC, OS.LINUX, OS.WINDOWS})
void test1(){
String testEnv = System.getenv("TEST_ENV");
System.out.println(testEnv);
assumeTrue("LOCAL".equalsIgnoreCase(testEnv));
Study study = new Study(10);
assertThat(study.getLimit()).isGreaterThan(9);
}
@Test @DisplayName("assumingThat")
@DisabledOnOs({OS.WINDOWS})
void test2(){
String testEnv = System.getenv("TEST_ENV");
assumingThat("LOCAL".equalsIgnoreCase(testEnv), () -> {
System.out.println("LOCAL");
Study study = new Study(10);
assertThat(study.getLimit()).isGreaterThan(9);
});
assumingThat("TEST".equalsIgnoreCase(testEnv), () -> {
System.out.println("TEST");
Study study = new Study(10);
assertThat(study.getLimit()).isGreaterThan(9);
});
}
}
@EnabledOnOs: 현재 테스트를 진행하는 운영체제가 명시되어 있으면 테스트를 진행
@DisabledOnOs: 현태 테스트를 진행하는 운영체제가 명시되어 있으면 테스트를 진행 하지 않는다.
실행 결과
현재 내 OS는 WINDOWS이다.
test1은 EnabledOnOS에 해당하는 WINDOWS가 있어서 실행되었다.
test2는 WINDOWS면 테스트를 실행하지 않아서 실행되지 않았다.
@EnabledOnJre
자바 버전에 따라 실행
@Test
@EnabledOnJre(JRE.JAVA_17)
void test3(){
System.out.println("test3");
}
@Test
@EnabledOnJre({JRE.JAVA_11, JRE.JAVA_8})
void test4(){
System.out.println("test4");
}
현재 자바 버전: 17
테스트3은 실행 테스트4는 실행되지 않는다.
@EnabledIfEnvironmentVariable
assumeTrue, assumingThat과 똑같은 효과
@Test
@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "LOCAL")
void test5(){
System.out.println("test5");
}
@Test
@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "TEST")
void test6(){
System.out.println("test6");
}
현재 환경 변수 TEST_ENV는 "LOCAL"값을 가진다.
그래서 테스트5는 실행 테스트6은 실행되지 않는다.
개인적으로 assumeTrue 보다 더 명시적인듯하다.
JUnit 5 태깅과 필터링
class TagTest {
@Test
@Tag("fast")
void test1(){
System.out.println("fast");
}
@Test
@Tag("slow")
void test2(){
System.out.println("slow");
}
}
실행 결과
fast 태그에 해당하는 테스트들만 실행되도록 설정해 보자
Edit Configuration에 들어가서 JUnit이 없으면 + 버튼을 눌러서 Junit을 찾고 추가하자
Tags 부분에 넣고 싶은 태그(fast)를 추가한다.
다시 실행
태그가 fast인 test1만 실행되었다.
git bash 프로젝트 전체 테스트 실행
./gradlew test
특정 테스트 클래스 실행
--test 옵션을 준다.
./gradlew test --tests com.example.thejavatest.TagTest
특정 테스트 메소드는 클래스명 뒤에 메소드명까지 붙이면 된다.
build.gradle 특정 테그를 가진 테스트만 실행하도록 설정 변경
tasks.named('test') {
useJUnitPlatform{
includeTags("fast", "slow")
// excludeTags("slow")
}
}
주석처럼 포함하고 싶지 않는 테그가 있으면 excludeTags()를 사용하자
실행: ./gradlew test
build/reports/tests/test/index.html
우클릭 & open in browser (chrome)
includeTags("fast") result
includeTags("fast", "slow") result
이렇게 태그로 필터링에 성공했다.
maven같이 다른 빌드 도구나 인텔리제이가 아닌 다른 IDE를 사용하면 아래 링크를 참고하자
https://junit.org/junit5/docs/current/user-guide/#running-tests-tag-expressions
JUnit 5 커스텀 태그
@FastTest - custom 애노테이션 / 태그
/**
* FastTest 애노테이션은
* @Test + @Tag("fast") 애노테이션을 결합한 composed 애노테이션
* 즉, 새로운 메타 애노테이션을 만든 것
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("fast")
public @interface FastTest {
}
@SlowTest
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("slow")
public @interface SlowTest {
}
왜 굳이 이렇게 custom 테그를 만들까?
@Tag("")이렇게만 해도 충분한데
@Tag 안에 들어가는 데이터는 문자열이다.
이는 사용자가 입력하기 때문에 테스트 시 타입 안정성이 없다.
반면에 커스텀 태그를 만들어서 적용하면 타입 안정성이 보장된다.
또한 코드의 가독성 및 유지보수성을 높인다.
JUnit 5 테스트 반복하기
@RepeatedTest(value)
class RepeatTest {
@RepeatedTest(10)
void repeatTest(RepetitionInfo repetitionInfo){
System.out.println("test "
+ repetitionInfo.getCurrentRepetition()
+ "/" + repetitionInfo.getTotalRepetitions());
}
}
@RepeatedTest 애노테이션 value로 test 반복횟수를 지정할 수 있다.
@RepeatedTest 애노테이션을 사용하면 메소드 매개변수로 RepetitionInfo 객체를 받을 수 있다.
getCurrentRepetition(): 현재 반복 회차
getTotalRepetitions(): 전체 반복 횟수
실행 결과
참고로 @Test에 RepetitionInfo를 매개변수로 받으려고 하면 NoArgumentException이 발생한다.
class RepeatTest {
@DisplayName("반복 테스트 연습")
@RepeatedTest(value = 10, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo){
System.out.println("test "
+ repetitionInfo.getCurrentRepetition()
+ "/" + repetitionInfo.getTotalRepetitions());
}
}
@RepeatedTest
value = 반복 횟수
name = 반복 테스트 이름 설정 {displayName}, {currentRepetition}, {totalRepetitions} 같은 값을 사용 가능
실행 결과
@ParameterizedTest
JUnit5에서 다양한 입력값을 사용하여 같은 테스트 메소드를 반복적으로 실행할 수 있도록 지원하는 애노테이션
이를 통해 코드 중복을 줄이고, 다양한 데이터 조합에 대해 테스트할 수 있다.
@DisplayName("ParameterizedTest 연습")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요"})
void parameterizedTest(String message){
System.out.println(message);
}
@ValueSource를 통해 문자를 입력값으로 넘길 수 있다.
@RepeatedTest와 마찬가지로 {displayName}을 사용 가능하고 {index}, message = {} 사용이 가능하다.
실행 결과
@ParameterizedTest에서 사용 가능한 여러가지 인자 값들의 소스에 대해서 알아보자
@ValueSource
String 뿐만 아니라 int, double, char 등 여러가지 타입이 가능하다.
@NullSource, @EmptySource, @NullAndEmptySource
@DisplayName("ParameterizedTest 연습")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요"})
@NullSource
@EmptySource
void parameterizedTest(String message){
System.out.println(message);
}
이전 테스트에서 @NullSource, @EmptySource 추가했다.
파라미터로 null, 빈 문자열이 추가된 걸 확인할 수 있었다.
참고로 @NullAndEmptySource만 추가해도 @NullSource, @EmptySource를 사용한 것과 같은 효과를 낸다.
@NullAndEmptySource는 @NullSource, @EmptySource를 가지는 컴포즈된 애노테이션이다.
int의 경우 @EmptySource, @NullSource 는 허용되지 않는다.
만약 잘못된 값을 사용하면 PreconditionViolationException이 발생한다.
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
@EmptySource
void test(int value){
System.out.println(value);
}
Integer로 받는 경우 @NullSource는 가능, @EmptySource 불가능
이는 암묵적인 타입 변환을 해준다.
SimpleArgumentResolver & @ConvertWith
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void test(@ConvertWith(StudyConverter.class) Study study){
System.out.println(study.getLimit());
}
static class StudyConverter extends SimpleArgumentConverter{
@Override
protected Object convert(Object source, Class<?> targetType) throws ArgumentConversionException {
assertEquals(Study.class, targetType, () -> "Can only convert to Study");
return new Study(Integer.parseInt(source.toString()));
}
}
SimpleArgumentConverter를 상속받는 클래스를 만들고
convert 메소드를 구현해서 원하는 객체 타입으로 argument를 변환이 가능하다.
이때 @ConertWith(Converter.class)로 명시해줘야 한다.
@CsvSource
다양한 형식의 데이터 세트를 매개변수화된 테스트에 주입할 수 있는 애노테이션이다.
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바'", "20, '스프링'"})
void cscSource(Integer limit, String name){
System.out.println(new Study(limit, name));
}
""를 통해 구분한다. 문자열은 작은 따옴표를 붙인다.
ArgumentAccessor 활용하기
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바'", "20, '스프링'"})
void cscSource2(ArgumentsAccessor argumentsAccessor){
System.out.println(
new Study(argumentsAccessor.getInteger(0),
argumentsAccessor.getString(1)));
}
참고로 ArgumentResolver는 1개의 Argument에 대해서만 가능하다.
ArgumentsAggregator & @AggregateWith 활용
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바'", "20, '스프링'"})
void cscSource3(@AggregateWith(StudyAggregator.class) Study study){
System.out.println(study);
}
static class StudyAggregator implements ArgumentsAggregator{
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
return new Study(accessor.getInteger(0), accessor.getString(1));
}
}
더 많은 내용은 아래 링크를 참고하자
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests
JUnit5 테스트 인스턴스
class TestInstance {
private int value = 0;
@Test
void test1(){
System.out.println(value++);
}
@Test
void test2(){
System.out.println(value++);
}
}
test1, test2는 모두 value를 출력하고 value값을 올린다.
test1, test2의 순서는 보장되지 않는다. 하지만 어떤 test에서는 1이 출력되야 한다. (만약 테스트 메소드를 실행하는 인스턴스가 같다면)
하지만 출력 결과는 모두 0 이다.
그렇다면 각 테스트를 실행하는 인스턴스는 다를까?
hashCode를 찍어보자
class TestInstance {
@Test
void test1(){
System.out.println(this.hashCode());
}
@Test
void test2(){
System.out.println(this.hashCode());
}
}
그렇다 테스트를 실행하는 서로 인스턴스가 다르다
왜 JUnit의 테스트 기본 전략은 각 테스트(메소드) 마다 인스턴스를 새로 생성해서 테스트했을까?
그 이유는 테스트 메소드를 독립적으로 실행하여 예상치 못한 부작용을 방지하기 위해서이다.
JUnit 5에서는 이 전략을 바꿀 수 있다.
각 테스트를 실행할 때마다 인스턴스를 생성하지 않고 재사용하면 성능 측면에서 좋을 것이다.
@TestInsatance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestInstancePractice {
@Test
void test1(){
System.out.println(this.hashCode());
}
@Test
void test2(){
System.out.println(this.hashCode());
}
}
이제 두 테스트가 같은 인스턴스를 통해서 실행된다
이전에 @BeforeAll, @AfterAll 같은 경우 static 메소드로 만들어야 한다고 했었다.
하지만 클래스에서 같은 인스턴스를 사용하는 전략인 경우 static 메소드로 만들지 않아도 된다.
JUnit5 테스트 순서
class Order {
@Test
void test1(){
}
@Test
void test2(){
}
@Test
void test3(){
}
}
실행할 테스트 메소드는 특정한 순서에 의해 몇번을 실행해도 동일한 순서로 실행된다.
하지만 어떻게 그 순서를 정하는지는 의도적으로 분명히 하지 않는다.
그 이유는 새로운 인스턴스를 만드는 이유와 동일하다.
하지만 경우에 따라서 특정한 순서대로 테스트를 실행하고 싶을 때도 있다.
ex) 회원가입, 회원 로그인, 회원 조회 같은 회원에 대한 통합 테스트를 순서대로 진행하고 싶을 때
그 방법에 대해서 알아보자
@TestMethodOrder
Collection 정렬할 때 Comparator를 넘겨주듯이 @TestMethodOrder에 정렬 기준을 넘겨준다.
다양한 구현체를 넘겨줄 수 있다.
MethodOrder.OrderAnnotation: @Order 애노테이션 기준
MethodOrder.Random: 무작위 실행
MethodOrder.Alphanumeric: 메소드 이름에 알파벳 및 숫자가 포함된 경우, 알파벳 순으로 정렬
MethodOrder.OrderAnnotation 사용해 보자
@Order(value) value가 작을 수록 더 높은 우선 순위를 가진다.
Spring에서 제공하는 @Order 애노테이션도 존재하는데 헷갈리지 말고 junit.jupitor 패키지에 있는 @Order를 사용하자
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderTest {
@Test
@Order(3)
void test1(){
}
@Test
@Order(2)
void test2(){
}
@Test
@Order(1)
void test3(){
}
}
JUnit 5 junit-platform.properties 설정 파일
Junit 설정 파일로, 클래스 패스 루트 (src/test/resources/)에 넣어두면 적용된다.
파일 생성
테스트 인스턴스 라이프사이클 설정
# 테스트 인스턴스 라이프사이클 설정
junit.jupiter.testinstance.lifecycle.default = per_class
per_class, per_method
만약 테스트에 @TestInstance가 있다면 해당 애노테이션의 설정이 우선 순위가 더 높다
확장팩 자동 감지 기능
# 확장팩 자동 감지 기능
junit.jupiter.extensions.autodetection.enabled = true
@Disabled 무시하고 실행하기
# @Disabled 무시하고 실행하기
junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition
class PropertiesTest {
@Test
@Disabled
void test1(){
}
@Test
void test2(){
}
}
@Disabled가 있지만 무시하고 실행된다.
테스트 이름 표기 전략 설정
# 테스트 이름 표기 전략 설정
junit.jupiter.displayname.generator.default = \
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
@Test
void test_study_is_fun(){
}
테스트 이름에 있는 underscore(_)를 공백으로 바꿨다.
JUnit 5 확장 모델
JUnit 4의 확장 모델은 @RunWith(Runner), TestRule, MethodRule
JUnit 5의 확장 모델은 Extension
확장팩 생성
이전 예제에서 테스트가 오래 걸리는 테스트를 구분하기 위해서 @Test와 @Tags 애노테이션을 컴포즈한
@SlowTest 애노테이션을 만들었었다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("slow")
public @interface SlowTest {
}
이번에는 테스트의 실행 시간이 내가 생각하는 시간 보다 더 길게 나오는 데 @SlowTest가 걸려있지 않는
테스트에 @SlowTest를 붙이라고 권장하는 메시지를 출력하는 확장팩을 만들어 보겠다.
FindSlowTestExtension
public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final long THRESHOLD = 1000L;
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
ExtensionContext.Store store = getStore(context);
store.put("START_TIME", System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
ExtensionContext.Store store = getStore(context);
long startTime = store.remove("START_TIME", long.class);
Method requiredTestMethod = context.getRequiredTestMethod();
SlowTest slowTest = requiredTestMethod.getAnnotation(SlowTest.class);
if(System.currentTimeMillis() - startTime > THRESHOLD && slowTest == null){
System.out.printf("Please consider mark method [%s] with @SlowTest.\n", requiredTestMethod.getName());
}
}
private static ExtensionContext.Store getStore(ExtensionContext context) {
String testClassName = context.getRequiredTestClass().getName();
String testMethodName = context.getRequiredTestMethod().getName();
ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));
return store;
}
}
BeforeTestExecutionCallback, AfterTestExecutionCallback 인터페이스를 구현한다.
beforeTestExecution 메소드
ExecutionContext에 들어있는 실행하는 테스트 클래스, 테스트 메소드 이름을 가지고 store를 가져온다.
그리고 store에 현재 시간을 넣는다.
afterTestExecution 메소드
똑같이 ExecutionContext에 들어있는 실행하는 테스트 클래스, 테스트 메소드 이름을 가지고 store를 가져온다.
store에 들어있는 시작 시간을 꺼내면서 지운다.
그리고 리플렉션을 사용해서 테스트 메소드에 붙어있는 @SlowTest를 가져온다.
물론 없으면 null 이다.
THRESHOLD에 지정한 시간보다 현재 시간 - 시작 시간이 더 크면 내가 생각하는 시간을 초과 했다.
하지만 이미 @SlowTest가 붙어 있으면 경고문의 의미가 없으니 null 이면 출력을 한다.
확장팩 등록 방법
선언적인 등록 @ExtendWith
@ExtendWith(FindSlowTestExtension.class)
class ExtensionTest {
@Test
void test1(){
System.out.println("test1");
}
@SlowTest
void test2() throws InterruptedException{
System.out.println("test2");
Thread.sleep(1005L);
}
@Test
void test3() throws InterruptedException{
System.out.println("test3");
Thread.sleep(1005L);
}
}
test2는 시간이 더 오래 걸리지만 이미 @SlowTest 애노테이션이 붙어 있어서 경고문이 출력되지 않는다.
하지만 test3는 @SlowTest 애노테이션도 아니고 시간도 오래 걸려서 경고문이 출력된다.
현재 방법도 좋지만 FindSlowTestExtention에 있는 THRESHOLD 값은 고정이다.
즉, 테스트마다 다른 값을 줄 수가 없다.
프로그래밍 등록 @RegisterExtension
FindSlowTestExtension 수정
이전에는 THRESHOLD 값이 고정이었지만 생성자로 받게 만들었다.
public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private long THRESHOLD;
public FindSlowTestExtension(long THRESHOLD) {
this.THRESHOLD = THRESHOLD;
}
// 생략 ...
class ExtensionTest {
@RegisterExtension
FindSlowTestExtension findSlowTestExtension = new FindSlowTestExtension(1000L);
@Test
void test1(){
System.out.println("test1");
}
@SlowTest
void test2() throws InterruptedException{
System.out.println("test2");
Thread.sleep(1005L);
}
@Test
void test3() throws InterruptedException{
System.out.println("test3");
Thread.sleep(1005L);
}
}
자동 등록 자바 ServiceLoader 이용
더 많은 내용은 아래 링크 참고
https://junit.org/junit5/docs/current/user-guide/#extensions
참고 자료
[인프런] 백기선님 더 자바, 애플리케이션을 테스트하는 다양한 방법
'Spring' 카테고리의 다른 글
[Mockito] 정리 (1) | 2024.11.27 |
---|