JetBrains에서 조사한 바(2023년 기준)로는 자바 개발자의 84%가 JUnit을 사용하며, 그 중 46›%는 Mockito를 사용한다고 한다.
자바 개발자가 가장 많이 사용하는 테스팅 프레임워크이다.
JUnit5는 JDK8 이상부터 지원을 한다.
SpringBoot 2.2.0 버전부터 기본으로 제공해주는 JUnit 버전을 5로 올리면서, JUnit 5에 대한 공부가 필수적이지 않아졌나!

JUnit 5은 다음과 같이 세부 모듈로 나누어져 있다.

SpringBoot 2.2.0 버전부터 spring-boot-starter-test가 JUnit 5를 기본적으로 제공해준다.
즉, SpringBoot 2.2.0 프로젝트를 사용한다면 자동으로 JUnit5 의존성이 추가되어 바로 사용가능하다.
https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
maven에서 원하는 JUnit5 버전을 선택 후, 자신의 빌드 도구에 맞춰 사용!
@Test : 해당 메소드가 테스트 메소드임을 나타내며, 해당 메소드가 실행되면서 자동으로 단위 테스트가 수행된다.@BeforeAll / @AfterAll : 모든 테스트가 실행되기 이전/이후에 딱 한번 호출된다.@TestInstance(TestInstance.LifeCycle.PER_CLASS)를 붙여 테스트 클래스의 생명주기를 클래스 단위로 바꿔주면 static을 사용하지 않아도 된다!@BeforeEach / @AfterEach : 각 테스트가 실행되기 이전/이후에 딱 한번 호출된다.@Disabled : 특정 메소드를 실행하고 싶지 않을 때 사용한다.class StudyTest {
@Test
void create1() {
Study study = new Study();
assertNotNull(study);
System.out.println("first create");
}
@Test
void create2() {
System.out.println("second create");
}
@Test
@Disabled
void create3() {
System.out.println("third create");
}
// 모든 테스트를 실행하기 이전에 딱 한번만 호출된다.
@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");
}
}

기본적으로는 메소드 이름으로 테스트 이름이 출력된다.
원하는 문자열로 테스트 이름을 보여주기 위한 방법은 다음과 같다.
@DisplayNameGeneration@DisplayName (권장)참고: https://junit.org/junit5/docs/current/user-guide/#writing-tests-display-names
org.junit.jupiter.api.Assertions.*
assertEquals(expected, actual)assertNotNull(actual)assertTrue(boolean)assertAll(executables...)assertAll()을 사용하여 한 번에 묶어주면, 모든 assert문에 대해 테스트가 성공하였는지 실패하였는지 알려준다.assertAll(
() -> assertNotNull(study),
() -> assertEquals(StudyStatus.DRAFT, study.getStatus(),
() -> "스터디를 처음 만들면 상태값이 " + StudyStatus.DRAFT + " 상태여야 한다."),
() -> assertTrue(study.getLimit() > 0, "스터디 최대 참석 가능 인원은 0보다 커야 한다.")
);

assertThrows(expectedType, executable)class Study {
...
public Study(int limit) {
if (limit < 0) {
throw new IllegalArgumentException("limit은 0보다 커야 한다.");
}
this.limit = limit;
}
...
}
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Study(-10));
assertEquals("limit은 0보다 커야 한다.", exception.getMessage());
assertTimeout(duration, executable)assertTimeout(Duration.ofMillis(100), () -> {
new Study(10);
Thread.sleep(300);
});

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

각 Assertion 메소드를 사용할 때, 마지막 매개변수로 문자열이나 Supplier 타입의 인스턴스를 람다 형태로 정보를 제공할 수 있다.
[문자열]
assertEquals(StudyStatus.DRAFT, study.getStatus(), "스터디를 처음 만들면 상태값이 " + StudyStatus.DRAFT + " 상태여야 한다.");
[Supplier 타입의 인스턴스]
assertEquals(StudyStatus.DRAFT, study.getStatus(), new Supplier<String>() {
@Override
public String get() {
return "스터디를 처음 만들면 상태값이 " + StudyStatus.DRAFT + " 상태여야 한다.");
}
});
[Supplier 타입의 인스턴스(람다식)]
assertEquals(StudyStatus.DRAFT, study.getStatus(), () ->"스터디를 처음 만들면 상태값이 " + StudyStatus.DRAFT + " 상태여야 한다.");
문자열을 그대로 사용한다면, 해당 테스트가 성공하든 실패하든 문자열 연산이 무조건 실행된다.
그러나 인스턴스를 생성하는 방식은, 해당 테스트가 실패하였을 경우에만 문자열 연산이 실행되기 때문에 연산 횟수를 줄일 수 있다.
따라서, 인스턴스를 람다 형태로 작성하는 것을 권장한다.
추가적으로 AssertJ, Hemcrest, Truth 등의 라이브러리를 사용할 수도 있다.
특정한 조건을 만족하는 경우에 테스트를 실행하는 방법은 다음과 같다.
org.junit.jupiter.api.Assumptions.*
assumeTrue(조건)// 환경변수가 LOCAL인지 확인한다.
String test_env = System.getenv("TEST_ENV");
System.out.println(test_env);
assumeTrue("LOCAL".equalsIgnoreCase(test_env));
// 그렇다면 실행된다.
Study actual = new Study(10);
assertThat(actual.getLimit()).isGreaterThan(0);
assumingThat(조건, 테스트)// 환경변수가 LOCAL인지 확인하고, 그렇다면 실행된다.
String test_env = System.getenv("TEST_ENV");
assumingThat("LOCAL".equalsIgnoreCase(test_env), () -> {
System.out.println(test_env);
Study actual = new Study(10);
assertThat(actual.getLimit()).isGreaterThan(0);
});
@Enabled___ 와 @Disabled___ @Test
@DisplayName("스터디 만들기 ~~")
@EnabledOnOs({OS.MAC, OS.LINUX}) // 맥, 리눅스 운영체제에서만 실행 O
void create1() {
System.out.println("fisrt create");
}
@Test
@DisplayName("스터디 만들기 !!")
@DisabledOnOs(OS.MAC) // 맥 운영체제에서는 실행 X
void create2() {
System.out.println("second create");
} @Test
@DisplayName("스터디 만들기 ~~")
@EnabledOnJRE({JRE.JAVA_11, JRE.JAVA_8}) // JAVA11, JAVA8에서만 실행 O
void create1() {
System.out.println("fisrt create");
} @Test
@DisplayName("스터디 만들기 ~~")
@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "LOCAL") // 환경변수가 일치하면 실행 O
void create1() {
System.out.println("fisrt create");
}테스트 그룹을 만들고 원하는 테스트 그룹만 테스트를 실행할 수 있는 기능이 있다.
@Tag



test {
useuseJUnit()
includeGroups 'fast', 'slow'
}
참고
JUnit 5 애노테이션을 조합하여 커스텀 태그를 만들 수 있다.
@Target(ElementType.METHOD) // 메소드에서 사용 가능
@Retention(RetentionPolicy.RUNTIME) // 런타임동안에는 해당 어노테이션을 유지
@Test // 테스트 기능
@Tag("fast") // 태그는 fast
public @interface FastTest {
}
@Retention(RetentionPolicy.RUNTIME) : 테스트가 실행하는 시점에도 해당 애노테이션을 참조할 수 있어야 하기 때문에, 런타임 시점에도 남아있어야 한다.
@FastTest // 커스텀 어노테이션을 사용
@DisplayName("스터디 만들기 fast")
void create1() {
Study actual = new Study(10);
assertThat(actual.getLimit()).isGreaterThan(0);
}
랜덤값을 사용하거나, 테스트를 실행하는 타이밍에 따라 달라지는 조건이 있다면 테스트를 반복하여 실행하는 도움이 될 수 있다.
@RepeatedTest@DisplayName("스터디 만들기")
@RepeatedTest(value = 10, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) { // RepetitionInfo 인자를 받아 정보 출력이 가능
System.out.println("test" + repetitionInfo.getCurrentRepetition() + "/"
+ repetitionInfo.getTotalRepetitions());
}

@ParameterizedTest@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요"})
void parameterizedTest(String message) {
System.out.println(message);
}

@ValueSource단일 값 또는 문자열 리터럴의 배열을 제공한다.
정수, 문자열 등의 값을 테스트 메서드에 전달할 때 사용한다.
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testWithValueSource(int value) {
// 테스트 로직
}
@NullSource, @EmptySource, @NullAndEmptySourcenull, 빈 문자열, null과 빈 문자열을 테스트 메서드에 전달할 때 사용한다.
@ParameterizedTest
@NullAndEmptySource
void testWithNullAndEmpty(String input) {
// 테스트 로직
}
@EnumSourceEnum 상수를 테스트 메서드에 전달할 때 사용한다.
@ParameterizedTest
@EnumSource(value = DayOfWeek.class, names = {"MONDAY", "WEDNESDAY", "FRIDAY"})
void testWithEnum(DayOfWeek day) {
// 테스트 로직
}
@MethodSource다른 메소드에서 제공되는 값을 테스트 메서드에 전달할 때 사용한다.
@ParameterizedTest
@MethodSource("provideStrings")
void testWithMethodSource(String input) {
// 테스트 로직
}
static Stream<String> provideStrings() {
return Stream.of("apple", "banana", "orange");
}
@CsvSourceCSV 형식의 문자열을 파싱하여 값을 테스트 메서드에 전달할 때 사용한다.
@ParameterizedTest
@CsvSource({"apple, 1", "banana, 2", "orange, 3"})
void testWithCsvSource(String fruit, int count) {
// 테스트 로직
}
@CvsFileSourceCSV 파일에서 값을 읽어와 테스트 메서드에 전달할 때 사용한다.
@ParameterizedTest
@CsvFileSource(resources = "/data.csv")
void testWithCsvFile(String fruit, int count) {
// 테스트 로직
}
@ArgumentSource사용자 지정 인수 소스에서 값을 가져와 테스트 메서드에 전달한다.
사용자 정의 소스는 ArgumentSource 인터페이스를 구현해야 한다.
@ParameterizedTest
@ArgumentSource(MyArgumentsProvider.class)
void testWithCustomArgumentSource(String argument) {
// 테스트 로직
}
SimpleArgumentConverter 상속 받은 구현체 제공
@ConvertWith
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(ints = {10, 20, 40})
void parameterizedTest(@ConvertWith(StudyConvertor.class) Study study) {
System.out.println(study.getLimit());
}
// 커스텀 클래스의 하나의 인자를 테스트 메소드가 전달받을 수 있도록 Convertor 작성
static class StudyConvertor 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()));
}
}
ArgumentsAccessor
커스텀 Accessor
- ArgumentsAggregator 인터페이스 구현
- @AggregateWith
DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바 스터디'", "20, '스프링'"})
void parameterizedTest(@AggregateWith(StudyAggregator.class) Study study) {
System.out.println(study);
}
// 커스텀 클래스의 여러 개의 인자를 테스트 메소드가 전달받을 수 있도록 Convertor 작성
static class StudyAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
throws ArgumentsAggregationException {
return new Study(accessor.getInteger(0), accessor.getString(1));
}
}
참고
class StudyTest {
int value = 1;
@FastTest
@DisplayName("스터디 만들기 fast")
void create1() {
System.out.println(this); // 이 테스트 메소드를 실행할 때, 인스턴스를 새로 생성
System.out.println(value++); // 그래서 그냥 1이 찍힘
Study actual = new Study(10);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@SlowTest
@DisplayName("스터디 만들기 slow")
void create2() {
System.out.println(this); // 이 테스트 메소드를 실행할 때, 인스턴스를 새로 생성
System.out.println(value++); // 여기도 그냥 1이 찍힘
System.out.println("second create");
}
}
@TestInstance(Lifecycle.PER_CLASS)@BeforeEach/@AfterEach를 static으로 설정하지 않아도 된다! default 메소드로 정의할 수도 있다.@BeforeEach/@AfterEach에서 초기화 할 필요가 있다.실행되는 테스트 메소드는 특정한 순서에 의해 실행되기는 하지만, 그 순서를 어떻게 정하는지는 분명하지 않다.
테스트 메소드마다 테스트 인스턴스를 새로 만드는 것처럼, 유닛 테스트 간의 의존성을 없애기 위해서이다.
그러나! 경우에 따라, 특정 순서대로 테스트를 실행해야하는 경우가 있다.
예를 들어, 회원가입 후 로그인을 하는 비즈니스 로직을 작성하는 경우라면 순서에 따라 테스트 메소드가 실행되어야 한다.
이러한 경우에는 테스트 메소드를 원하는 순서에 따라 실행하도록 @TestInstance(Lifecycle.PER_CLASS)와 함께 @TestMethodOrder를 사용할 수 있다.
@TestInstance(Lifecycle.PER_CLASS)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudyTest {
int value = 1;
@Order(2)
@FastTest
@DisplayName("스터디 만들기 fast")
void create1() {
System.out.println(this);
System.out.println(value++);
}
@Order(1)
@FastTest
@DisplayName("스터디 만들기 fast")
void create1() {
System.out.println(this);
System.out.println(value++);
}
}
[실행 결과]




테스트 인스턴스 라이프사이클 설정
확장팩 자동 감지 기능
@Disabled 무시하고 실행하기
테스트 이름 표기 전략 설정
JUnit 5는 JUnit4보다 확장하는 방법이 단순해졌다.
실행 시간이 오래 걸리는 테스트 메서드가 있을 때, @SlowTest 어노테이션을 붙이는 확장 모델을 만들어보자.
선언적인 등록 @ExtendWith
@ExtendWith(FindSlowExtension.class)
class StudyTest {
...
}
public class FindSlowExtension 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 {
Method requiredTestMethod = context.getRequiredTestMethod();
SlowTest annotation = requiredTestMethod.getAnnotation(SlowTest.class);
String testMethodName = requiredTestMethod.getName();
ExtensionContext.Store store = getStore(context);
long startTime = store.remove("START_TIME", long.class);
long duration = System.currentTimeMillis() - startTime;
if(duration > THRESHOLD && annotation == null) {
System.out.printf("Please consider mark method [%s] with @SlowTest\n", testMethodName);
}
}
private static Store getStore(ExtensionContext context) {
String testClassName = context.getRequiredTestClass().getName();
String testMethodName = context.getRequiredTestMethod().getName();
return context.getStore(Namespace.create(testClassName, testMethodName));
}
}
프로그래밍 등록 @RegisterExtension
class StudyTest {
...
@RegisterExtension
static FindSlowExtension findSlowExtension = new FindSlowExtension(1000L);
...
}public class FindSlowExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private long THRESHOLD;
public FindSlowExtension(long THRESHOLD) {
this.THRESHOLD = THRESHOLD;
}
...
}참고
기본적으로 SpringBoot 2.2.0 버전 이상 프로젝트를 생성하면 JUnit 5를 사용할 수 있다.
그러나, JUnit 4를 사용하기 위해서는 junit-vintage-engine을 의존성으로 추가해야지 사용할 수 있다.
→ JUnit 5의 junit-platform으로 JUnit 3과 4로 작성된 테스트를 실행할 수 있다.
| JUnit 4 | JUnit 5 |
|---|---|
| @Category(Class) | @Tag(String) |
| @RunWith, @Rule, @ClassRule | @ExtendWith, @RegisterExtension |
| @Ignore | @Disabled |
| @Before, @After, @BeforeClass, @AfterClass | @BeforeEach, @AfterEach, @BeforeAll, @AfterAll |
💡 인프런 강의 <더 자바, 애플리케이션을 테스트하는 다양한 방법> 수강 후 정리한 글입니다.
: https://www.inflearn.com/course/the-java-application-test/dashboard