더 자바, 애플리케이션을 테스트하는 다양한 방법
JUnit
카테고리 : 더 자바, 애플리케이션을 테스트하는 다양한 방법
JUnit
- JUnit
JUnit 5: 소개
자바 개발자가 가장 많이 사용하는 테스팅 프레임워크
- https://www.jetbrains.com/lp/devecosystem-2019/java/
- 단위 테스트를 작성하는 자바 개발자 93% JUnit를 사용한다.
- 자바 8 이상을 필요로 한다.
- 대체제 : TestNG, Spock 등등
- Platform : 테스트를 실행해주는 런처 제공, TestEngine API 제공
- Jupiter: TestEngine API 구현체로 JUnit 5 를 제공
- Vintage: JUnit 4 와 2 을 지원하는 TestEngine 구현체
- JUnit 4는 하나의 덩어리 jar로 들어오고 junit 이 참조하는 다른 라이브러리가 있다.
- JUnit 5는 그 차체로 모듈화가 되어있다.(Platform, Jupiter, Vintage)
참고
JUnit 5 : 시작하기
- Test 메소드에 public 없어도 실행 가능하다.
- 리플렉션 사용하기 때문에
스프링 부트 프로젝트 만들기
- 2.2+ 버전의 스프링 부트 프로젝트를 만든다면 기본으로 JUnit 5 의존성 추가됨
스프링 부트 프로젝트 사용하지 않는다면
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
기본 애노테이션
- @Test
- @BeforeAll
- 여러 테스트가 모두 실행이 될 때 모든 테스트를 실행하기 전에 한 번만 실행
- 반드시 static 메서드 사용 private 은 안되고 default는 됨 static void 기억하면 된다.
- @AfterAll
- 여러 테스트가 모두 실행이 될 때 모든 테스트를 실행한 후에 한 번만 실행
- 반드시 static 메서드 사용 private 은 안되고 default는 됨 static void 기억하면 된다.
- @BeforeEach
- 여러 테스트가 모두 실행이 될 때 각각의 테스트 실행하기 전에 실행
- static 일 필요 없다.
- @AfterEach
- 여러 테스트가 모두 실행이 될 때 각각의 테스트 실행한 후에 실행
- static 일 필요 없다.
- @Disabled
- 테스트 중에 실행 안 하기 설정 메서드
JUnit 5 : 테스트 이름 표시하기
- 기본 표기 전략은 메서드 이름 표시
@DisplayNameGeneration
- Method 와 Class 레퍼런스를 사용해서 테스트 이름ㅇ르 표기하는 방법 설정
- 기본 구현체로 ReplaceUnderscores 제공
@DisplayName
- 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 어노테이션
- @DisplayNameGeneration 보다 우선 순위가 높다.
소스
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@Test
@DisplayName("스터디 만들기 ╯°□°)╯")
void create_new_study() {
Study study = new Study();
assertNotNull(study);
System.out.println("create");
}
@Test
@DisplayName("스터디 만들기 \uD83D\uDE31")
void create_new_study_again() {
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");
}
}
참고
JUnit 5 : Assertion
org.junit.jupiter.api.Assertions.*
- 실제 값이 기대한 값과 같은지 확인
- assertEquals(expected, actual)
- 값이 null 아닌지 확인
- assertNotNull(actual)
- 다음 조건이 참(true)인지 확인
- assertTrue(boolean)
- 모든 확인 구문 확인
- assertAll(executables…)
- 예외 발생 확인
- assertThrows(expectedType, executable)
- 특정 시간 안에 실행이 완료되는지 확인
- assertTimeout(duration, executable)
- assertTimeout 설정한 시간을 기다렸다가 비교하고 종료한다
- assertTimeoutPreemptively 설정한 시간을 기다리지 않고 종료한다
- 주의해서 사용해야함 ThreadLocal 을 사용하는 코드가 있다면 예상치 못한 결과가 나타날수 있다.
- 스프링 transaction 은 ThreadLocal 을 기본 전략으로 사용하는데 ThreadLocal 은 설정을 안 하면 다른 스레드에서 공유를 할 수 없다 그때는 스프링 transaction 설정이 제대로 적용이 안될 수 있다 테스트에서 스프링은 transaction 한 테스트를 rollback 을 기본으로 하는데 롤백이 안되고 db에 반영되는 경우가 발생할 수 있다.
- 마지막 매개변수로 Supplier
타입의 인스턴스를 람다 형태로 제공할 수 있다, - 복잡한 메시지 생성해야 하는 경우 사용하면 실패한 경우에만 해당 메시지를 만들게 할 수 있다,
- AssertJ, Hemcrest, Truth 등의 라이브러리를 사용할 수도 있다.
JUnit 5 : 조건에 따라 테스트 실행하기
특정 조건을 만족하는 경우에 테스트를 실행하는 방법
- org.junit.jupiter.api.Assumptions.*
- assumeTrue(조건)
- assumingThat(조건, 테스트)
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@Test
@DisplayName("스터디 만들기 ╯°□°)╯")
void create_new_study() {
assumeTrue("Local".equalsIgnoreCase(System.getenv("TEST_ENV")));
Study actual = new Study(100);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@Test
@DisplayName("스터디 만들기 \uD83D\uDE31")
void create_new_study_again() {
assumingThat("Local".equalsIgnoreCase(System.getenv("TEST_ENV")), () -> {
System.out.println("create1");
});
}
}
- @Enable___ 와 @Disabled___
- OnOS
- OnJre
- IfSystemProperty
- IfEnvironmentVariable
- If
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@Test
@DisplayName("스터디 만들기 ╯°□°)╯")
@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "LOCAL")
void create_new_study() {
Study actual = new Study(100);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@Test
@DisplayName("스터디 만들기 \uD83D\uDE31")
@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "test")
void create_new_study_again() {
System.out.println("create1");
}
@Test
@EnabledOnOs({OS.MAC, OS.LINUX})
void test(){
System.out.println("test");
}
@Test
@EnabledOnJre({JRE.JAVA_8, JRE.JAVA_11})
void test3(){
System.out.println("test3");
}
@Test
@DisabledOnOs({OS.MAC, OS.LINUX})
void test2(){
System.out.println("test2");
}
@Test
@DisabledOnJre(JRE.OTHER)
void test4(){
System.out.println("test2");
}
}
JUnit 5 : 태깅과 필터링
테스트 그룹을 만들고 원하는 테스트 그룹만 테스트를 실행할 수 있는 기능.
- @Tag
- 테스트 메소드에 태그를 추가할 수 있다.
- 하나의 테스트 메소드에 여러 태그를 사용할 수 있다.
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@Test
@DisplayName("스터디 만들기 fast")
@Tag("fast")
void create_new_study() {
Study actual = new Study(100);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@Test
@DisplayName("스터디 만들기 slow")
@Tag("slow")
void create_new_study_again() {
System.out.println("create1");
}
}
- 인텔리 J 에서 특정 태그로 테스트 필터링 하는 방법
- edit configuration -> test kind -> Tags 로 설정 후 Tag 이름 지정
- 메이븐에서 테스트 필터링 하는 방법
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<groups>fast | slow</groups>
</configuration>
</plugin>
<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>
<profile>
<id>ci</id>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
참고
- https://maven.apache.org/guides/introduction/introduction-to-profiles.html
- https://junit.org/junit5/docs/current/user-guide/#running-tests-tag-expressions
JUnit 5 : 커스텀 태그
JUnit 5 애노테이션을 조합하여 커스텀 태그를 만들 수 있다.
- FastTest.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@FastTest
@DisplayName("스터디 만들기 fast")
void create_new_study() {
Study actual = new Study(100);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@SlowTest
@DisplayName("스터디 만들기 slow")
void create_new_study_again() {
System.out.println("create1");
}
}
JUnit 5 : 테스트 반복하기 1부
@RepeatedTest
- 반복 횟수와 반복 테스트 이름을 설정할 수 있다.
- {displayName}
- {currentRepetition}
- {totalRepetitions}
- RepetitionInfo 타입의 인자를 받을 수 있다.
@ParameterizedTest
- JUnit 5 는 기본으로 제공
- 테스트에 여러 다른 매개변수를 대입해가며 반복 실행한다.
- {displayName}
- {index}
- {arguments}
- {0}, {1}, …
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@DisplayName("스터디 만들기")
@RepeatedTest(value = 10, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) {
System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" +
repetitionInfo.getTotalRepetitions());
}
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요."})
void parameterizedTest(String message) {
System.out.println(message);
}
}
JUnit 5 : 테스트 반복하기 2부
인자 값들의 소스
- @ValueSource
- @NullSource, @EmptySource, @NullAndEmptySource
- @EnumSource
- @MethodSource
- @CsvSource
- @CvsFileSource
- @ArgumentSource
인자 값 타입 변환
- 암묵적인 타입 변환
- 레퍼런스 참고
- 명시적인 타입 변환
- SimpleArgumentConverter 상속 받은 구현체 제공
- @ConvertWith
인자 값 조합
- ArgumentsAccessor
- 커스텀 Accessor
- ArgumentsAggregator 인터페이스 구현
- @AggregateWith
소스
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class StudyTest {
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTest0(ArgumentsAccessor accessor) {
Study study = new Study(accessor.getInteger(0), accessor.getString(1));
System.out.println(study);
}
@DisplayName("스터디 만들기")
@RepeatedTest(value = 10, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) {
System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" +
repetitionInfo.getTotalRepetitions());
}
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@CsvSource({"10, '자바 스터디'", "20, 스프링"})
void parameterizedTest(@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));
}
}
@DisplayName("스터디 만들기")
@ParameterizedTest(name = "{index} {displayName} message={0}")
@ValueSource(ints = {10, 20, 30})
void parameterizedTest2(@ConvertWith(StudyConverter.class) Study study) {
System.out.println(study);
}
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()));
}
}
}
참고
JUnit 5 : 테스트 인스턴스
JUnit 은 테스트 메소드 마다 테스트 인스턴스를 새로 만든다.
- 이것이 기본 전략
- 테스트 메소드를 독립적으로 실행하여 예상치 못한 부작용을 방지하기 위함이다.
- 이 전략을 JUnit 5에서 변경할 수 있다.
@TestInstance(Lifecycle.PER_CLASS)
- 테스트 클래스당 인스턴스를 하나만 만들어 사용한다.
- 경우에 따라, 테스트 간에 공유하는 모든 상태를 @BeforeEach 또는 @AfterEach 에서 초기화 할 필요가 있다.
- @BeforeAll 과 @AfterAll 을 인스턴스 메소드 또는 인터페이스에 정의한 default 메소드로 정의할 수도 있다.
소스
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class StudyTest {
int value = 1;
@Test
@DisplayName("스터디 만들기")
void create_new_study() {
System.out.println(this);
System.out.println(value++);
Study actual = new Study(1);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@Test
@DisplayName("스터디 만들기")
void create_new_study_again() {
System.out.println(this);
System.out.println("create1 " + value++ );
}
}
JUnit 5 : 테스트 순서
- 실행할 테스트 메소드 특정한 순서에 의해 실행되지만 어떻게 그 순서를 정하는지는 의도적으로 분명히 하지 않는다.
- 테스트 인스턴스를 테스트 마다 새로 만드는 것과 같은 이유
- 경우에 따라, 특정 순서대로 테스트를 실행하고 싶을 때도 있다. 그 경우에는 테스트 메소드를 원하는 순서에 따라 실행하도록 @TestInstance(TestInstance.Lifecycle.PER_CLASS)와 함께 @TestMethodOrder 를 사용할 수 있다.
- MethodOrder 를 사용할 수 있다.
- 기본 구현체
- Alphanumeric
- OrderAnnotation
- Random
- 소스
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudyTest {
int value = 1;
@Order(2)
@Test
@DisplayName("스터디 만들기 fast")
void create_new_study() {
System.out.println(this);
System.out.println(value++);
Study actual = new Study(1);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@Order(1)
@Test
@DisplayName("스터디 만들기 slow")
void create_new_study_again() {
System.out.println(this);
System.out.println("create1 " + value++);
}
@Order(3)
@DisplayName("스터디 만들기")
@RepeatedTest(value = 10, name = "{displayName}, {currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) {
System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" +
repetitionInfo.getTotalRepetitions());
}
}
JUnit 5 : junit-platform.properties
- JUnit 설정 파일로, 클래스패스 루트 (src/test/resources)에 넣어두면 적용된다.
테스트 인스턴스 라이프사이클 설정
- junit.jupiter.testinstance.lifecycle.default = per_class
확장팩 자동 감지 기능
- junit.jupiter.extensions.autodetection.enable = true
@Disabled 무시하고 실행하기
- junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition
테스트 이름 표기 전략 설정
- junit.jupiter.displayname.generator.default = org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
JUnit 5: 확장 모델
- JUnit 4의 확장 모델은 @RunWith(Runner). TestRule, MethodRule
- JUnit 5의 확장 모델은 단하나, Extension
확장팩 등록 방법
- 선언적인 등록 @ExtendWith
- 프로그래밍 등록 @RegisterExtension
- 자동 등록 자바 ServiceLoader 이용
확장팩 만드는 방법
- 테스트 실행 조건
- 테스트 인스턴스 팩토리
- 테스트 인스턴스 후 처리기
- 테스트 매개변수 리졸버
- 테스트 라이프 사이클 콜백
- 예외 처리
- 등등
소스
public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private long THRESHOLD = 1000L;
public FindSlowTestExtension(long THRESHOLD) {
this.THRESHOLD = THRESHOLD;
}
@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 start_time = store.remove("START_TIME", long.class);
long duration = System.currentTimeMillis() - start_time;
if (duration > THRESHOLD && annotation == null) {
System.out.printf("Please consider mark method [%s] with @SlowTest.\n", testMethodName);
}
}
private ExtensionContext.Store getStore(ExtensionContext context) {
String testClassName = context.getRequiredTestClass().getName();
String testMethodName = context.getRequiredTestMethod().getName();
return context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));
}
}
@ExtendWith(FindSlowTestExtension.class) // 선언적 방법
class StudyTest {
int value = 1;
@RegisterExtension
static FindSlowTestExtension findSlowTestExtension = // 프로그래밍 등록
new FindSlowTestExtension(1000L);
@Test
void create_new_study() {
System.out.println(this);
System.out.println(value++);
Study actual = new Study(1);
assertThat(actual.getLimit()).isGreaterThan(0);
}
@Test
void create_new_study_again() throws InterruptedException {
Thread.sleep(1005L);
System.out.println(this);
System.out.println("create1 " + value++);
}
@Test
void repeatTest(RepetitionInfo repetitionInfo) {
System.out.println("test " + repetitionInfo.getCurrentRepetition() + "/" +
repetitionInfo.getTotalRepetitions());
}
}
참고
JUnit 5 : JUnit 4 마이그레이션
- junit-vintage-engine 을 의존성으로 추가하면, JUnit 5 의 junit-platform 으로 JUnit 3과 4로 작성된 테스트를 실행할 수 있다.
- @Rule 은 기본적으로 지원하지 않지만, junit-jupiter-migrationsupport 모듈이 제공하는 @EnableRuleMigrationSupport 를 사용하면 다음 타입의 Rule 을 지원한다.
- ExternalResource
- Verifier
- ExpectedException
JUnit 4 | JUnit 5 |
---|---|
@Category(Class) | @Tag(String) |
@RunWith, @Rule, @ClassRule | @ExtendWith, @RegisterExtension |
@Ignore | @Disabled |
@Before, @After, @BeforeClass, @AfterClass | @BeforeEach, @AfterEach, @BeforeAll, @AfterAll |