JUnit 5 연습하기 - 001

5 분 소요

JUnit 5 연습하기

인프런 강좌 “더 자바, 애플리케이션을 테스트하는 다양한 방법” 에서 Junit 5 부분을 발췌

강좌 URL https://www.inflearn.com/course/the-java-application-test/

소개

JUnit 5 는 자바 개발자가 가장 많이 사용하는 테스팅 프레임워크

JUnit 5 의 세부모듈

  • Jupiter : TestEngine API 구현체로 JUnit 5 를 제공 -> 우리가 주로 사용할 구현체
    • Jupiter 의 친구들 : AssertJ, Hemcrest, Truth
  • JUnit Platform : 실행가능한 런처 제공, TestEngine API 제공
  • Vintage : JUnit 4, 3 를 지원하느 구현체

시작하기

스프링프레임워크 프로젝트 만들기

https://start.spring.io/ 에서 Spring web 추가하여 생성

테스트하고자 하는 파일에서 cmd + shift + t 로 테스트파일 생성 가능

스프링부트 프로젝트가 아니라면?

maven 의존성 추가

단 java 1.8 이상이어야 함

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.5.2</version>
    <scope>test</scope>
</dependency>

테스트 실행 방법

메소드명에서 control + shift + r 누르면 테스트 실행 가능

어노테이션 소개

@BeforeAll, @AfterAll

모든 테스트를 실행하기 전, 후에 딱 한번 실행됨

메소드를 static void 로 작성해야 함

@BeforeEach, @AfterEach

각 테스트가 실행 전, 후에 실행

@Disabled

메소드에 선언하면 테스트 실행안함

주석말고 사용하자

테스트 이름

@DisplayName

테스트 이름을 쉽게 표현할 수 있는 어노테이션

권장됨

@DisplayNameGeneration

전략에 따라 DisplayName 을 생성하는 어노테이션

전략을 담은 클래스를 임포트

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) // UnderScore 를 공백으로 만들어줌

Assertion

여러가지 Assertion

비교항목 assert
실제 값이 기대한 값과 같은지 확인 assertEqulas(expected, actual)
값이 null이 아닌지 확인 assertNotNull(actual)
다음 조건이 참(true)인지 확인 assertTrue(boolean)
모든 확인 구문 확인 assertAll(executables…)
예외 발생 확인 assertThrows(expectedType, executable)
특정 시간 안에 실행이 완료되는지 확인 assertTimeout(duration, executable)

assertion 의 왼쪽이 기대값, 오른쪽이 실제값

assertEquals(expected, actual);

마지막에 Supplier 클래스가 들어갈 수 있음

assertEquals(expected, actual, new Supplier<String>() {
    @Override
    public String get() {
        return "스터디를 처음 만들면 상태값이 DRAFT 여야 한다.";
    }
})

람다식으로 줄일 수 있음

assertEquals(StudyStatus.DRAFT, study.getStatus(), "스터디를 처음 만들면 상태값이 DRAFT 여야 한다.");

// 람다식 사용
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면 상태값이 DRAFT 여야 한다.");

그냥 문자열로 써도 되는데 람다식으로 쓰는 이유는?

-> 오류발생 시에만 문자열 연산을 해주기 위해

assertAll

연관된 테스트를 한번에 하는 방법

assertAll(
    () -> assertNotNull(study),
    () -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면 상태값이 DRAFT 여야 한다."),
    () -> assertTrue(study.getLimit() > 0, "스터디 최대참석인원은 0보다 커야 한다.");
);

assertThrows

예외처리를 테스트하는 방법

assertThrows(IllegalArgumentException.class, () -> new Study(-10));

// 메시지 확인 가능
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Study(-10));
String message = exception.getMessage();
assertEquals("limit 는 0 보다 커야 한다.", message);

assertTimeout

소요시간을 테스트하는 방법

assertTimeout(Duration.ofMillis(100), () -> {
    new Study(10);
    Thread.sleep(300);
});

타임아웃이 되면 바로 테스트종료 시키기

assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
    new Study(10);
    Thread.sleep(300);
});

assertj

jupiter Assertion 외에 AssertJ, Hemcrest, Truth 등의 라이브러리를 사용할 수도 있다.

assertj 의 assertThat 예시

import static org.assertj.core.api.Assertions.assertThat;
...
void create_new_study(){
    Study actual = new Study(10);
    assertThat(actual.getLimit()).isGreaterThan(0);
}

조건에 따라 테스트

assumeTrue

특정한 버전이나 OS나 환경에 따라 다르게 테스트를 실행하는 방법

assumeTrue("LOCAL".equalsIgnoreCase(System.getenv("TEST_ENV")));

assumeTrue 의 결과가 true 인 경우에만 이 후 테스트 실행함

System.getenv 의 값은 ~/.zshrc 등에서 export TEXT_ENV=LOCAL 식으로 설정할 수 있음

assumingThat()

assumingThat("LOCAL".equalsIgnoreCase("TEST_ENV"), () -> {
    Study actual = new Study(10);
    assertThat(actual.getLimit()).isGreaterThan(0);
});

@Enabled, @Disabled

@EnabledOnOS OS 에 따라 실행

@DisabledOnOS OS 에 따라 실행안함

@EnabledOnOs({OS.MAC, OS.LINUX})

@EnabledOnJre 특정 버전에 따라 실행

@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_9})

@EnabledIfSystemProperty 시스템속성에 따라 실행

@EnabledIfEnvironmentVariable 환경변수에 따라 실행

JUnit 5 테스트 반복하기

@RepeatedTest

테스트를 반복시키는 어노테이션

어노테이션에서 cmd + p 입력하면 파라미터 확인가능

@DisplayName("스터디 반복")
@RepeatedTest(value = 10, name="{displayName}, {currentRepetition}/{totalRepetitions}")

@ParameterizedTest

테스트에 파라미터를 넣고 파라미터만큼 테스트를 반복시키는 어노테이션

@DisplayName("파라미터 테스트")
@ParameterizedTest(name="{index} {displayName} message={0}")
@ValueSource(strings = {"날씨가", "정말, "많이", "더워졌습니다"})

인자값 source

기본적으로 valueSource 로 다양한 값을 전달할 수 있음

@ValueSource(strings = {"날씨가", "정말, "많이", "더워졌습니다"})

인자값의 소스를 정하는 다양한 어노테이션이 있음

소스에 따라

  • @ValueSource : 다양한 값 전달 가능
      @ValueSource(ints = {10, 20, 40})
    
  • @NullSource, @EmptySource, @NullAndEmptySource : 파라미터가 없는 테스트
  • @EnumSource
  • @MethodSource
  • @CsvSource
      @CsvSource({"10, '자바 스터디'", "20, 스프링"})
    
  • @CvsFileSource
  • @ArgumentSource

인자값 타입 변환

  • https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-argument-conversion-implicit

SimpleArgumentConverter

인자를 객체로 받고 싶다?

-> SimpleArgumentConverter 상속 받은 구현체를 생성

static class StudyConverter extends SimpleArgumentConverter {
    @Override
    protected Object convert(Object source, Class<?> targetType) throws ArgumentConversionException {
        assertEquals(Study.class, targetType, "Can Only ...");
        return new Study(Integer.parseInt(source.toString()));
    }
}

@ConvertWith 사용

@ValueSource(strings = {"날씨가", "정말, "많이", "더워졌습니다"})
void parameterizedTest(@ConvertWith(StudyConverter.class) Study study) {
    System.out.println(study.getLimit());
}

하나의 argument 를 받을 때는 argumentConverter 를 사용

argumentAccessor

두개의 argument 를 받을 때는 2개 인자로 받거나

@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTest(Integer limit, String name) {
    Study study = new Study(limit, name);
}

argumentAccessor 사용

@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTest(ArgumentsAccessor argumentsAccessor) {
    Study study = new Study(argumentsAccessor.getInteger(0), argumentsAccessor.getString(1))
}

ArgumentsAggregator

ArgumentsAggregator 구현하여 사용할 수도 있음

static class StudyAggregator implements ArgumentsAggregator {
    @Override
    public Object aggregateArguments(ArgumentsAccessor argumentsAccessor, ParameterContext parameterContext) throws ArgumentsAggregationException {
        return new Study(argumentsAccessor.getInteger(0), argumentsAccessor.getString(1));
    }
}

@AggregateWith 사용

@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTest(@AggregateWith(StudyAggregator.class) Study study) {
    System.out.println(study.getLimit());
}

ArgumentsAggregator 는 public static 클래스로 구현해야함

  • https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

JUnit 5 테스트 인스턴스

JUnit 은 테스트 메소드마다 테스트 인스턴스를 만드는 것이 기본 전략이다.

그래서 테스트 순서를 완전히 보장할 수는 없다.

이 전략을 JUnit 5 에서 변경할 수 있다.

TestInstance

@TestInstance 를 선언하여 테스트 클래스당 하나의 인스턴스로 사용할 수 있다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StudyTest {
    ...
}

PER_CLASS 선언 시 테스트 메소드간의 상태를 공유할 수 있음

@BeforeEach, @AfterEach 로 초기화 할 수도 있다.

PER_CLASS 를 선언하면 @BeforeAll, @AfterAll 이 static 일 필요가 없다.

JUnit 5 테스트 순서 정하기

경우에 따라, 특정 순서대로 테스트를 실행하고 싶을 때, 테스트 메소드를 원하는 순서에 따라 실행하도록 할 수 있다.

@TestInstance(Lifecycle.PER_CLASS) 와 함께 @TestMethodOrder 사용

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudyTest {
    ...
}

MethodOrderer 구현체를 설정하여 사용할 수 있다.

  • Alphanumeric
  • OrderAnnoation
  • Random

메소드에 @Order 어노테이션 선언하여 사용

@Order(1)
void Test() {
    ...
}

@TestMethodOrder 를 사용하면 @BeforeAll, @AfterAll 이 static 으로 선언되어야 한다.

단위테스트라면 순서가 필요없고, 유스케이스 기반의 테스트는 순서가 중요하다.

참고