Junit5

반응형
반응형

*백기선의 더자바 애플리케이션을 테스트하는 다양한 방법을 바탕으로 작성 했습니다.!

 

더 자바, 애플리케이션을 테스트하는 다양한 방법 - 인프런

자바 프로그래밍 언어를 사용하고 있거나 공부하고 있는 학생 또는 개발자라면 반드시 알아야 하는 애플리케이션을 테스트하는 다양한 방법을 학습합니다. 초급 프로그래밍 언어 프레임워크

www.inflearn.com

Junit5은 자바의 테스팅 프레임워크 이다.

구성

Junit Platform + Junit Jupiter + JUnit Vintage 으로 구성되어 있다.

Junit Platform : TestEngine Api 추상체 , 실행을 위한 런처 제공
 Junit Jupiter : Junuit5의 구현체 
 JUnit Vintage : Junit3,4의 구현체

JUnit Vintage가 필요한 이유 : Junit3,4가 기반인 프로젝트를 실행시키기 위해서.

특징

  - 모듈을 통해 관리를 한다.
  - 자바 JDK8이상에서 사용이 가능하다.

시작하기

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>

api

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

test engine

 

실행 테스트

public class Study {
    public int add(int value) {
        return value;
    }
}

간단한 코드를 작성한다.

command + shift + t 

class StudyTest {

    @Test
    public void add() {
        Study study = new Study();
       assertEquals(study.add(5),5);

    }
}

값을 넣으면 그 값이 맞는지 확인하는 간단한 코드

제대로 실행 되었다.

그러면 이제 시작해보자.

주요 어노테이션

      @Test : 메서드가 테스트임을 나타낸다. (위에 작성함)

     @DisplayNameGeneration : 메소드나 클래스에  사용 할 수 있으며, 테스트 이름에 전략을 사용할 수 있다.    

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model will not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and cus

junit.org

    @DisplayName : 테스트 사용자 정의 이름을 표시합니다.

    @Test
    @DisplayName("안녕?")
    public void add() {
        Study study = new Study();
       assertEquals(study.add(5),5);

    }

옆을 보세요.

@AfterAll , @BeforeAll

 - 딱 한번 프로그램이 시작하거나 끝날때 실행이 된다. 
 -  static을 사용해야 한다.

@AfterEach, @BeforeEach

  - 테스트 구문이 실행될때마다 실행된다.
 - 굳이 static일 필요는 없다.

@BeforeAll
    static void all1() {
        System.out.println("전체");
    }

    @AfterAll
    static void all2() {
        System.out.println("끝내기");
    }

    @BeforeEach
    void each() {
        System.out.println("각각");
    }
    @AfterEach
    void each2() {
        System.out.println("끝 각각");
    }
    

비어 있는 곳이 테스트 구문

@Disabled : 테스트 무시 

 

Assertion

  assertXXX를 이용해서 테스트를 실행 한다.

    - assertEquals : 테스트할 값과 실제 값이 같은지 테스트를 실행한다, 

@Test
    void same() {
        Study study = new Study();
        assertEquals(study.value(3),5, "값이 서로 다릅니다.");
    }

  추가적으로 string이나 supplier<String>을 추가 할 수 있다.

인텔리제이에서는 supplier<String>를 사용하는 것 보다 String을 사용하는 것을 권장한다.

   - assertFalse / assertTrue
      boolean값이 틀린지/ 맞는지 테스트를 실행한다,

assertFalse(study.setBoolean(true),"이것은 틀려야 합니다.");

   

- assertThrows 
   예외가 발생하는지 테스트 합니다.

@Test
    void error() {
        assertThrows(
                IllegalArgumentException.class,
                () -> new Integer(1 / 0)
                , "0으로는 나눌 수 없습니다.");
    }

 

- assertTimeout 
   시간내에 끝나는지 확인합니다. 다만 에러가 종료 될때까지 기다립니다.

@Test
    void time() {
        assertTimeout(Duration.ofSeconds(3),() -> Thread.sleep(4000) , "3초 이내에 끝나야 합니다.");
    }

4초 24

- assertAll 
  여러 테스트를 동시에 실행이 가능한다.

@Test
    void all() {
        assertAll(() -> assertEquals(3, 5, "값이 서로 다릅니다."),
                  () -> assertFalse(true, "이것은 틀려야 합니다.") ,
                  () -> assertThrows(
                          IllegalArgumentException.class,
                          () -> new Integer(1 / 0)
                          , "0으로는 나눌 수 없습니다."),
                 () -> assertTimeout(Duration.ofSeconds(3),() -> Thread.sleep(4000) , "3초 이내에 끝나야 합니다."));
    }

여기서 궁금증! 이렇게 하는건 어떨까?

    @Test
    void all() {assertEquals(3, 5, "값이 서로 다릅니다.");
                   assertFalse(true, "이것은 틀려야 합니다.");
                  assertThrows(
                          IllegalArgumentException.class,
                          () -> new Integer(1 / 0),
                          "0으로는 나눌 수 없습니다.");
                  assertTimeout(Duration.ofSeconds(3),() -> Thread.sleep(4000) , "3초 이내에 끝나야 합니다.");
    }

에러가 발생되면 바로 테스트는 종료된다.

 

태깅과 필터링

@Tag를 이용해서 그룹을 지정할 수 있다.

@Test
@Tag("fast")
void add2() {
    Study study = new Study();
    assertEquals(study.value(3), 5, "값이 서로 다릅니다.");
 }

이제 tag가 fast인 테스트만 실행하게 만들 수 있다.

만약 ,IDE를 인텔리제이를 사용한다면?

Edit Configurations
이런식으로 fast인것만 실행된다. 
전체

 

커스텀 태그

Junit5의 에노테이션는 메타 에노테이션으로 사용할 수 있습니다.
ex) @Tag

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface MyFastTest {
}

@Fast 커스텀 태그를 만드는 커스텀 에노테이션입니다.

@Test
    @MyFastTest
    void error() {
        IllegalArgumentException value = assertThrows(
                IllegalArgumentException.class,
                () -> new Integer(1 / 0)
                , "0으로는 나눌 수 없습니다.");
    }

이런식으로 설정해둡니다.

@Test
    @Tag("fast")
    void flag() {
        Study study = new Study();
        assertFalse(study.setBoolean(true), "이것은 틀려야 합니다.");
    }

혹시 모르니 @Tag("fast")는 하나는 남겨두겠습니다.

실행!

테스트 반복하기

 1. 단순하게 반복 하기 : @RepeatedTest

@RepeatedTest(10)
    void repeat() {
        System.out.println("repeat");
    }

이렇게 10번이 반복되었다.

하지만 이것은 가독성이 떨어진다.

@RepeatedTest(value = 10,name = "이름 표시")
    void repeat() {
        System.out.println("repeat");
    }

이렇게 하면 이름을 지정할 수 있는데 

{currentRepetition} : 현재 반복 횟수 
{totalRepetitions} : 총 반복 횟수

지금은 테스트 창에서는 나오지만 콘솔?창에는 나오지 않고 있다. 이러한 정보는

TestInfo 또는 RepetitionInfo에 저장되어있다.

특히 getDisplayName같은 경우, 위에서 지정한 반복 테스트 이름의 문자열을 조작하는 방법을 제공한다.
하지만 반복 횟수나 총 횟수는 알 수 없었다. 

그러면 RepetitionInfo에서는 알 수 있을까?

이거 콘솔 창이예요.

따라서 리피트 정보를 알고 싶을 때는 RepetitionInfo에서 찾아보고, 테스트 정보를 찾고 싶으면 TestInfo을 찾아보면 알 수 있다.

2. 단순 반복이 아닌, 인수를 통해 반복하고 싶은 경우,

ex) {"문자열1","문자열2","문자열3"}을 반복 싶다면?  반복문을 통해 index를 구한다?

@ParameterizedTest를 사용한다.

@ParameterizedTest(name = "???")
    @ValueSource(strings = { "안녕하세요", "저는", "누구일까요?" })
    void palindromes(String str) {
        System.out.println(str);
}

심심해서 name을 추가해봣다.
name에 {index} => 횟수 , {displayName} => 메소드 명 , message={0} => 인자값 

정상적으로 나오는게 확인되었다.

 

테스트 인스턴스 

Junit은 테스트를 실행할때마다 인스턴스를 재 생성하게 된다. 
따라서 맴버 변수를 1로 설정해두고 그 값을 1씩 계속 증가시켜도 
항상 1인 이유가 바로 그 때문이다.

    int value = 0;


    @Test
    @DisplayName("같은가?")
    void add() {
        System.out.println(value++);
        Study study = new Study();
        assertEquals(study.value(3), 3);
    }

    @Test
    @Tag("fast")
    void flag() {
        System.out.println(value++);
        assertFalse(false, "이것은 틀려야 합니다.");
    }

오른쪽을 보세요.

Junit이 이러한 결정을 한 이유는 테스트간에 의존성을 최소화하기 위함이다. 그래서 @BeforeAll 이나 페이스북 @AfterAll을 사용할때 static을 사용한 이유도 이 때문이다. 하지만 인스턴스를 계속 생성해야 하기 때문에 성능적으로 봤을때는 좋지 않을 수 도 있다.

Junit5부터는 Junit의 기본전략을 수정할 수 있다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

클래스당 인스턴스를 생성한다는 의미다. 이렇게 하면 @BeforeAll이나 기자 @AfterAll에 static을 붙일 필요는 없어졌다.

같은 코드로 실행해보자.

값이 변경되었다. (같은 코드임)

 

테스트 순서

 Junit의 테스트 순서는 순차적으로 실행된다. (몇번을 실행해도 같은 결과를 도출한다.)
 이 결과는 Junit내부에서 얼마든지 바뀔 수 있다.
평소에는 테스트가 어떻게 실행되는지 상관은 없다.
왜냐하면 테스트는 각각 독립적이야 되기 때문이다. 

    @Test
    @DisplayName("같은가?")
    void add() {
        System.out.println(value++);
        Study study = new Study();
        assertEquals(study.value(3), 3);
    }

    @Test
    @Tag("fast")
    void flag() {
        System.out.println(value++);
        assertFalse(false, "이것은 틀려야 합니다.");
    }

    @Test
    void hell() {
        System.out.println("hello?");
    }

    @Test
    void next() {
        System.out.println("다음");
    }
    @Test
    void good() {
        System.out.println("좋아요");
    }

만약, 일반적인 테스트가 아니라 시나리오테스트 같은 경우는 어떨까?

시나리오 테스트 같은 경우는 순서가 매우 중요하다.
또한, 시나리오 테스트는 테스트간의 의존성이 있어야 한다. 그래야 원하는 테스트를 진행 할 수 있기 때문이다.

먼저 위에서 학습한 클래스당 테스트 인스턴스를 생성하게 변경한뒤,
직접 테스트 순서를 변경할 수 있다.

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

@TestMethodOrder 어노테이션으로 설정할 수 있는데 다음과 같이 추가할 수 있다.
조금전에 실행했던 방법과 비교하면 좋을 것 같다.

OrderAnnotation : 어노테이션으로 지정된 방법대로 순서를 정한다.
크기는 int의크기를 따른다.

@Test
    @DisplayName("같은가?")
    @Order(3)
    void add() {
        System.out.println(value++);
        Study study = new Study();
        assertEquals(study.value(3), 3);
    }

    @Test
    @Tag("fast")
    @Order(4)
    void flag() {
        System.out.println(value++);
        assertFalse(false, "이것은 틀려야 합니다.");
    }

    @Test
    @Order(5)
    void hell() {
        System.out.println("hello?");
    }

    @Test
    @Order(2)
    void next() {
        System.out.println("다음");
    }
    @Test
    @Order(1)
    void good() {
        System.out.println("좋아요");
    }

만약 Order어노테이션이 없다면? 맨 나중으로 실행 된다.

순서가 변경되었다.

다른 것들은 자세하게 다루지는 않겠지만,

DisplayName.class : 개발자가 지정한 테스트 명의 알파벳 순서에 따라 테스트를 실행한다.
MethodName.class : 개발자가 지정한 메소드 명의 알파벳 순서에 따라 테스트를 실행한다.
Random.class : 랜덤

 

확장 모델

Junit을 확장시켜 사용할 수 있다.
예를들어,  시간이 조금 걸리는 테스트에는 @FastTest를 붙이도록 권장하고 싶다면, 확장 모델을 사용해서 해결 할 수 있다.

Junit5에서는 Extention을 이용해서 확장 시킬 수 있다. 여러 모델?이 필요 했던 다른 버전에 비해 단순해진 느낌을 받는다.

등록하는 방법은 총 3가지를 제공하고 있다.

그전에 어떤식으로 확장시킬지에 대한 코드가 필요합니다.

public class TimeExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {

    }
    @Override
    public void afterTestExecution(ExtensionContext context) throws Exception {

    }

}

테스트가 시작할때, 어떻게 동작하는지, 끝날때, 어떻게 동작해야하는지 코드를 작성하자.

이 확장 클래스의 핵심은 시간이다. 따라서 프로그래밍상에 시간에 관련된 내용이 추가되야 한다.
ExtensionContext안에는 각종 정보를 알수 있다.

클래스도 확인이 가능하고, 메소드, 테스트 명등을 알 수 있다.

또한, 그 정보들을 Store에 저장시킬 수 있다. 

@Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {
        long start = System.currentTimeMillis();
        String name = context.getDisplayName();
        String className = context.getClass().getName();
        ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(name,className));
        store.put("START_TIME",start);
    }

이제 이 정보들을 뺄수 도 있는데 이 때는 remove를 사용한다.

@Override
    public void afterTestExecution(ExtensionContext context) throws Exception {
        String name = context.getDisplayName();
        String className = context.getClass().getName();
        ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(name,className));
        Long time = store.remove("START_TIME", long.class);
    }

 이렇게 나온 정보는 현재 시간과의 차이를 통해 얼마나 걸리는지 알 수 있다.

long duration = System.currentTimeMillis() - time;

10ms보다 작으면 동작하게 만들었다.

if (duration < 10) {
            System.out.printf("시간이 너무 빠릅니다. @FastTest를 붙여주세요. 현재 %s ms",duration);
}

1. 선언적으로 등록하기

    @ExtendWith

@ExtendWith(FastExtension.class)
class StudyTest {

이렇게 추가하면 10ms보다 빠른 테스트는 위 문장을 실행하게 된다. 그런데 이미 @FastTest가 붙어 있으면 나오지 않게 하고 싶다.
이것도 ExtensionContext안에 있을까?

자바의 리플렉션기능을 사용하면 해결 할 수 있다.

 MyFastTest annotation = context.getRequiredTestMethod().getAnnotation(MyFastTest.class);
        if (duration < 10 && annotation == null ) {
            System.out.printf("시간이 너무 빠릅니다. @FastTest를 붙여주세요. 현재 %s ms",duration);
        }

 

이 어노테이션이 null일때만 실행하게 변경하였다.

2. 프로그래 매틱 한 방법으로 등록

@RegisterExtension

이 방법은 각 테스트클래스마다 다르게 적용하고 싶을때 사용된다.
A테스트 클래스에는 5ms미만으로 싶고, B테스트 클래스에는 10ms미만으로 하고 싶다면 어떻게 해야할까?

int thread;

public FastExtension(int thread) {
    this.thread = thread;
}

이렇게 하면 각 테스트 클래스마다 적용할 수 있다. 하지만 바로 실행하게 되면 에러가 발생한다.

이럴때 사용되는 어노테이션이 @RegisterExtension이다.

@RegisterExtension
static FastExtension fast = new FastExtension(1);

이제 이 테스트 클래스는 1ms보다 빠르면 코드가 동작한다.

마지막 방법은 설정파일에 추가하는 방법이 있다.

이 밖에도 확장 모델 생명 주기,예외처리등 다양한 확장 방법을 제공하고 있다.

강의를 들으면서 작성하는데, 빠진 부분도 있어서 추후에 다시 추가할 예정입니다. ㅎㅎ;; 

 

반응형

'프로그래밍 언어 > 자바' 카테고리의 다른 글

스택 구현(설명x)  (0) 2020.12.07
Live-Study 대시 보드 만들기.  (0) 2020.12.06
반복문과 조건문  (0) 2020.11.29
백기선님 스터디에서 배운거 2가지 (간략히 정리)  (0) 2020.11.29
연산자  (0) 2020.11.26

댓글

Designed by JB FACTORY