Junit 5 테스트
Junit4를 잘 알고 있던건 아니지만 새로 입사한 회사에서 Junit5를 사용하여 테스트 코드를 짜기때문에 더 잘 알고 싶어 공부하게 되었다. 그 중 백기선님의 Junit5 테스트 코드 관련 인강을 인프런에서 듣게 되었다. 내용이 너무 좋았고 그동안 몰랐고 정리가 되지 않았던 부분을 많이 알게 되었다. 이를 아주 간략하게만 정리해봤다. 가격이 그리 비싸지 않기 때문에 한번쯤은 꼭 보는걸 추천한다.
https://www.inflearn.com/course/the-java-application-test/#
소개
- Junit5는 Junit3, 4에서 사용하던 Junit Platform 구현체 Vintage대신 Jupiter를 사용해서 TestEngine Api를 사용하는 test 프레임워크이다.
- Spring Boot 2.2.x가 릴리즈된 이후로는 공식적으로 Spring-boot-starter-tester에 제공되고 있다.
- Junit4까지는 test 코드가 public 하여야 했지만 Junit5 부터는 클래스, 메소드 모두 public하지 않아도 된다.
추가된 기능
Test 이름 지정
테스트의 이름을 지정할 때 기존에 _(언더스코어)를 사용하여 많이 작명하였었다.
@Test
public void when_join_not_error() {
}
하지만 이와 같은 방식으로 사용하게 되면 테스트가 실행되었을 때 내용을 보는데 많이 불편하다.
이런 불편함을 junit5의 Display 전략을 이용하면 편하게 볼 수 있다.
첫 번째 방법으로 @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)을 사용하여 _(언더스코이)이름을 언더스코어를 제거한 이름으로 만들어 줄 수 있다.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class WedulTest {
@Test
void when_join_not_error() {
}
}
하지만 단순하게 _(언더스코어)만 바꿔줬다고 가시적으로 보이지는 않는다. 그래서 테스트 이름을 직접 지정해 줄 수 있다.
@Test
@DisplayName("가입시 에러가 발생하는지 테스트")
void when_join_not_error() {
}
Assert 방법
기본적으로 Junit5에서 제공하는 Assert는 아래와 같은 방식으로 사용할 수 있다.
- expected : 기대하는 결과물
- actual : 실제 값
- message : 기대하는 결과물과 실제 값이 달랐을 때 발생될 문자
예를 들어 Wedul이라는 객체에 id를 가져왔을 때 1L인지 테스트하고 아니면 "아이디가 다릅니다." 메시지가 호출하게 해보자.
@Test
@DisplayName("가입시 에러가 발생하는지 테스트")
void when_join_not_error() {
Wedul wedul = new Wedul();
assertEquals(wedul.getId(), 1L, "아이디가 다릅니다.");
}
하지만 이렇게 작업 하는 것 보다 테스트를 편하게 도와주는 AssertJ 라이브러리를 사용해서 테스트 하면 더욱 편하다. 실제로 이 방식으로 팀에서도 하고 있어서 사용하는데 편했다.
assertThat(wedul.getId).isEqual(1L);
isEqual 이외에도 isNotNull, isNull 등 다양하게 확인이 가능하다.
특정 조건이 맞는 경우만 테스트 진행
assumeTrue, assumingThat를 사용하여 조건이 일치 할 때만 테스트를 진행하도록 할 수 있다.
// 시스템 환경설정의 profile이 dev일 때만 밑에 기능 테스트 가능!
assumeTrue("dev".equalsIgnoreCase(profile));
// assumingTest를 통해 특정 조건이 가능했을 때, 다음파라미터의 테스트 가능
assumingThat("test".equals("test"), () -> {
assertThat(study.getLimit()).isEqualTo(0);
});
만약 조건이 맞지 않으면 다음과 같이 테스트가 중지된다.
특정 동작하는 테스트들만 그룹화하여 테스트 하기
테스트 코드를 만들었을 때 특정 동작을 하는 테스트 코드들만 그룹화하고 필요 시 이들만 테스트 하고 싶을 수 있다. 이때 @Tag("그룹명")을 통해 그룹화 할 수 있다.
@Test
@DisplayName("가입 오류 테스트")
@Tag("quick")
void when_join_not_error() {}
위와 같이 @Tag로 묶고 해당 태그가 붙은 테스트만 실행 시키고 싶은 경우에 Run/Debug Configurations 다이얼로그를 띄우고 Test Kind를 Tag로 변경한 뒤 Tag Expression에 테스트 하고자하는 태그명을 적어주면 된다.
커스텀 태그 만들기
test를 위해서 태그를 붙이다 보면 동일한 동작을 하는 test method들에 동일한 태그를 반복해서 붙여줘야할 때가 있다. 아주 귀찮다. 이를 해결하기 위해 커스텀 태그를 만들어서 사용할 수 있다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("quick")
@Test
public @interface QuickTag {
}
// 이렇게 공통 태그를 묶어서 커스텀 태그를 만들 수 있다.
@DisplayName("가입 오류 테스트")
@QuickTag()
void when_join_not_error() {
Wedul wedul = new Wedul();
assertEquals(wedul.getId(), 1L, "아이디가 다릅니다.");
}
반복 테스트하기
테스트를 몇회 이상 반복하고 싶을 때 @RepeatedTest 어노테이션을 사용해서 테스트 할 수 있다. 그리고 RepetitionInfo를 매개변수로 받아 현재 반복 정보를 확인 할 수 있다. 그리고 name에 별도 인자값을 주어 현재 반복 테스트 정보를 이용하여 테스트 이름을 만들 수 있다.
@DisplayName("반복 테스트")
@RepeatedTest(value = 10, name = "{currentRepetition}/{totalRepetition} {displayName}")
void repeatTest(RepetitionInfo repetitionInfo) {
System.out.println("반복 테스트");
}
Parameter를 받아서 테스트 하기
테스트를 진행할 parameter를 받아서 테스트를 진행 할 수 있다. 이때 파라미터로 보내는 메서드는 static하고 이름이 같아야 한다.
@ParameterizedTest(name = "{index} {displayName} message={0}")
@MethodSource()
void parameterTest(String str) {
System.out.println(str);
}
static Stream<Arguments> parameterTest() {
return Stream.of(
Arguments.of("cjung"),
Arguments.of("wedul")
);
}
마찬가지로 @ParameterizedTest value에 이름을 index, index 등으로 테스트 정보를 기입할 수 있다.
Parameter 조작하여 테스트 하기
테스트에 들어오는 parameter를 조작하여 사용할 수 있다. 우선 SimpleArgumentConverter를 사용하면 들어온 데이터를 바로 다른 타입으로 변경해서 파라미터로 사용할 수 있게 해준다.
@ParameterizedTest(name = "{index} {displayName} message={0}")
@MethodSource()
void parameterConvertTest(@ConvertWith(WedulConverter.class) Wedul wedul) {
System.out.println(wedul.getId());
}
static Stream<Arguments> parameterConvertTest() {
return Stream.of(
Arguments.of("1"),
Arguments.of("2")
);
}
static class WedulConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) throws ArgumentConversionException {
assertThat(source.getClass()).isEqualTo(String.class);
return new Wedul(Long.parseLong(source.toString()));
}
}
그리고 Aggregator를 이용하여 들어온 파라미터들을 합쳐서 파라미터를 제공해줄 수 있다.
@ParameterizedTest(name = "{index} {displayName} message={0}")
@MethodSource()
void parameterAggregatorTest(@AggregateWith(WedulAggregator.class) Wedul wedul) {
System.out.println(wedul.getId());
System.out.println(wedul.getBalance());
}
static Stream<Arguments> parameterAggregatorTest() {
return Stream.of(
Arguments.of("1", 21),
Arguments.of("2", 41)
);
}
static class WedulAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context) throws ArgumentsAggregationException {
return new Wedul(accessor.getLong(0), accessor.getInteger(1));
}
}
테스트 인스턴스
테스트 간의 의존관계가 있기 때문에 클래스에 있는 모든 테스트들은 서로 다른 객체에서 실행된다. 그렇기 때문에 테스트에서 클래스 내부에 있는 인스턴스를 접근해서 값을 변경해도 다른 테스트에서 해당 데이터를 접근하면 기존 값으로 되어있다. 진짜 그런지 인스턴스 변수 값을 조작해서 찍어보고 객체의 hash값을 찍어보자.
public class WedulTestInstance {
int value = 1;
@Test
void test_1() {
System.out.println(this);
System.out.println(value++);
}
@Test
void test_2() {
System.out.println(this);
System.out.println(value++);
}
}
해시값도 같고 인스턴스 변수도 변하지 않는다는 걸 볼수 있다. 이를 해결하기 위해서 @TestInstance(TestInstance.Lifecycle.PER_CLASS)를 클래스에 지정하여 클래스당 인스턴스를 하나만 만들게 할 수 있다. 이러면 value도 변하고 해시값도 같은 걸 확인할 수 있다.
테스트 순서
테스트의 순서를 경우에 따라 지정하고 싶은 경우에는 클래스에 어노테이션으로 @TestMethodOrder를 사용하여 지정하는데 그 구현체로는 MethodOrder에는 OrderAnnotation, Alphanumeric, Random이 존재한다. 그리고 각 메서드에 @Order(순서)를 지정하여 진행할 수 있다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class TestOrder {
@Test
@Order(0)
void second() {
System.out.println(1);
}
@Test
@Order(1)
void first() {
System.out.println(2);
}
}
테스트 전역 properties
전역으로 설정가능한 테스트 속성을 추가하여 사용할 수 있다. 위치는 test/resources에 junit-Platform.properties 파일을 만들어서 진행하고 인텔리제이에 resources를 추가한다. 만약 메소드에 더 우선순위가 높은 애노테이션이 붙어있으면 그 설정이 우선이 된다.
## 대표 설정 값들
// 클래스마다 하나의 인스턴스 생성 (적용)
junit.jupiter.testinstance.lifecycle.default = per_class
// Disabled 무시하고 실행하기
junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition
Junit5 확장팩
Junit5에서 사용 할 확장 모델을 만들어서 테스트를 편하게 만들 수 있다. 사용할 때는 클래스에 @ExtendWith(확장팩 클래스.class)를 통해서 진행할 수 있다. 확장팩에 사용할 수 있는 리스트는 다음과 같다.
Extension을 만들고 사용하는 부분은 이곳 참조.
https://github.com/weduls/junit5/blob/master/java-junit5-study-junit/src/test/java/com/wedul/javajunit5studyjunit/extensions/FindSlowTestExtension.java
변경된 기능
Junit4에서는 @Before, @BeforeClass, @After, @AfterClass를 사용하여 테스트 사용 전, 후에 대하여 setup등을 진행하였다. Junit5에서도 동일한 기능을 제공하는데 이름만 변경되었다.
Junit4 | Junit5 | 기능 |
---|---|---|
@Before | @BeforeEach | 테스트 마다 실행되기전 실행 |
@BeforeClass | @BeforeAll | 테스트 클래스 당 테스트 전 실행되는 메서드, static 메서드 (Test Instance 전략 변경 시 non static 가능) |
@After | @AfterEAch | 테스트 마다 실행된 후 실행 |
@AfterClass | @AfterAll | 테스트 클래스 당 테스트 후 실행되는 메서드, static 메서드 (Test Instance 전략 변경 시 non static 가능) |