백기선님 자바스터디 4주차 : 제어문 2) 과제0. JUnit 5 학습

bongf·2021년 7월 29일
0

Java강의

목록 보기
5/18

과제 0. JUnit 5 학습하세요.

  • 아래 내용은 https://blog.jetbrains.com/idea/2020/09/writing-tests-with-junit-5/ 를 보며 따라 학습했다.
  • 출처를 따라하며 assertEqual을 사용했으나 사실 aseertThat을 사용하는 것이 더 직관적으로 읽기 쉽고, 타입 안정성을 가진다. 지난 번에 정리했던 글
  • 목차
    • 0.1. 환경설정
    • 0.2. 간단한 테스트
    • 0.3. 여러 개의 Assertions, assertAll
    • 0.4. Assumptions
    • 0.5. 예외 체크
    • 0.6. JUnit5 어노테이션
      • 어노테이션1 : @BeforeEach 와@BeforeAll 의 차이
      • 어노테이션2 : @TestInstance
      • 어노테이션3 : @Disabled
      • 어노테이션4 : @DisplayName
      • 어노테이션5 : @ParameterizedTest
      • 어노테이션6 : @Nested

1. 환경설정

  • JUnit5를 실행하기 위해서는 빌드툴(Gradle, Maven)이 필요하다.
  • gradle을 사용해서 프로젝트 만들기
  • 초기 설정된 dependency
    • 별도의 지정이 없어도 기본적으로 작성된다.
  • 출처에 따르면 dependency에 compile 'org.junit.jupiter:junit-jupiter-api:5.7.2' 가 추가하라고 하는데 그것이 없어도 잘 작동된다.
    • Gradle projects 세팅을 Gradle로 했을 때는 문제 없었는데 IntelliJ IDEA로 바꾸니까 작동이 안되었다. 출처에 따라서 dependecy에 추가해주니 작동 잘되었다

2. 간단한 테스트

  • Junit5는 Assertins 클래스가 있다.
  • 테스트 : assertEquals(좌, 우) 좌항과 우항이 같은지 test
public class ExampleTest {
    @Test
    void shouldShowSimpleAssertion() {
        Assertions.assertEquals(1, 1);
        Assertions.assertEquals(1, 2);
    }
}
  • 결과 : 이렇게 테스트가 틀렸을 경우 기댓값과 실제값을 알려준다

3. 여러 개의 Assertions, assertAll

  • 기본적으로 하나의 테스트 메소드 내에 여러개의 assertions을 넣을 수 있다.
    • 테스트 :
    @Test
    @DisplayName("Should check all items in the list")
    void shouldCheckAllItemsInTheList() {
        List<Integer> numbers = List.of(2, 3, 5, 7);
        Assertions.assertEquals(2, numbers.get(0));
        Assertions.assertEquals(3, numbers.get(1));
        Assertions.assertEquals(5, numbers.get(2));
        Assertions.assertEquals(7, numbers.get(3));
    }
    • 결과 :
  • 그러나 도중에 하나의 test가 실패하면 우리는 다른 테스트의 결과가 실패인지 통과인지 알수가 없다.
    • 테스트 : 실패할 assertions을 포함하여 작성
    @Test
    @DisplayName("Should check all items in the list")
    void shouldCheckAllItemsInTheList() {
        List<Integer> numbers = List.of(2, 3, 5, 7);
        Assertions.assertEquals(2, numbers.get(0));
        Assertions.assertEquals(3, numbers.get(3));
        Assertions.assertEquals(5, numbers.get(2));
        Assertions.assertEquals(7, numbers.get(3));
    }
    • 결과 : 결과에 보여지는 테스트 외에 다른 테스트는 통과인지 실패인지 알 수가 없다.
  • 그래서 assertAll이 있다. 도중에 하나가 실패해도 모든 assertion을 체크하기 때문이다. 람다식으로 표현해서 assertAll로 묶을 수 있다.
    • 테스트 : assertAll 을 활용한 테스트
    	@Test
    	@DisplayName("Should check all items in the list")
    	void shouldCheckAllItemsInByAssertAll() {
        List<Integer> numbers = List.of(2, 3, 5, 7);
        Assertions.assertAll(
                () -> assertEquals(1, numbers.get(0)), //실패 
                () -> assertEquals(3, numbers.get(1)),
                () -> assertEquals(1, numbers.get(2)), //실패 
                () -> assertEquals(7, numbers.get(3)));
    	}
    • 결과 : 두 개의 assertEquals가 fail되게 작성했을 때 이 모든 것을 잡아주는 것을 알 수 있다.

4. Assumptions

  • 특정 assumptions(가정)이 true일 때만 test를 하고 싶을 때 사용한다.

    • 테스트 : 현재 날짜를 받아서 이것이 "WENDSDAY"일 때만 실행하게 하는 코드
    public class ExampleTest {
    	private final String currentDayOfWeek = LocalDate.now().getDayOfWeek().toString();
    
    	@Test
    	@DisplayName("Should only run the test if some criteria are met")
    	void shouldOnlyRunTheTestIfSomeCriteriaAreMet() {
        	       	Assumptions.assumeTrue(currentDayOfWeek.equals("WEDNESDAY"));
       		assertEquals(1, 1);
    	}
    }
    • 결과 : assumptionstrue이기 때문에 테스트가 잘 통과된다
    • 테스트 : assumptions을 틀리게 작성
     @Test
        @DisplayName("Should only run the test if some criteria are met")
        void shouldOnlyRunTheTestIfSomeCriteriaAreMet() {
            Assumptions.assumeTrue(currentDayOfWeek.equals("TUESDAY"));
            assertEquals(1, 1);
        }
    • 결과 : assertEquals 가 아예 실행되지 않는다

5. 예외 체크

  • 유효하지 않은 input이 있을 때 발생하는 예외를 체크할 수 있다.
  • java-chess 프로젝트를 하면서 input 값에 말의 색깔이 흰색, 검정 색 외에 다른 색이 들어오면 예외가 발생하는 코드를 작성했다. 이 때, 예외체크 기능을 활용하여 pink라는 색을 입력했을 때 작성해둔 예외가 발생하는지 확인하는 test를 작성했던 기억이 난다.
    • 테스트 : 예외가 발생하도록 작성한 코드
      @DisplayName("Should throw exception")
        @Test
        void shouldThrowException() {
            assertThrows(RuntimeException.class, () -> {
                Integer.parseInt("hello");
            });
        }
    • 결과 : 기대한 예외 발생하여 테스트 통과
  • AssertJ 사용한 예외 체크 : assertThatThrownBy 사용
 	@Test
        @DisplayName("잘못된 위치에 node를 추가하면 예외가 발생해야한다")
        void addNodeAtWrongPosition() {
            assertThatThrownBy(() -> {
                linkedList.add(head, new ListNode(1), 5);
            }).isInstanceOf(IndexOutOfBoundsException.class);

        }

6. JUnit5 어노테이션

어노테이션설명자세한 설명
@Test테스트메소드라는 것을 알려준다
@ParameterizedTest파라미터라이즈 테스트라는 것을 알려준다. 매개변수 전달하여 테스트docs
@RepeatedTest동일한 테스트를반복할 때docs baeldung
@TestMethodOrder테스트 실행 순서 정하기. @TestInstance 를 사용할 때 순서 정할 때 사용, 순서를 정하는 방법에는 여러 옵션이 있다.docs
@TestInstance길어서 아래 설명docs
@DisplayName테스트에 이름 붙이기
@BeforeEach동일 클래스 내에서 각각의 @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory 가 붙은 메소드 실행 전 실행
@AfterEach동일 클래스 내에서 각각의 @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory 가 붙은 메소드 실행 후 실행
@BeforeAll모든 all @Test, @RepeatedTest, @ParameterizedTest, and @TestFactory 메소드 전에 한번만 실행
@Nested테스트 묶어줄 때 사용 , @BeforeAll and @AfterAll는 @TestInstance로 클래스 단위의 환경변수를 만들어주지 않는 한 사용 불가
@Disabled이 테스트는 무시하세요

6.1 어노테이션 1 : @BeforeEach@BeforeAll 의 차이.

각각의 테스트 전에 한번씩 실행 된다는 것(@BeforeEach)과 모든 테스트 전에 한 번씩 실행되는 결과(@BeforeAll )의 차이를 보기 위해 코드를 작성했다.

  • 테스트 : @BeforeEach
@DisplayName("A stack")
public class BeforeEachTest {
    Stack<Object> stack;
    String anElement = "an element";

    @BeforeEach
    void createNewStack() {
        stack = new Stack<>();
        stack.push(anElement);
    }

    @Test
    @DisplayName("it is no longer empty")
    void isNotEmpty() {
        assertThat(stack.isEmpty()).isFalse();
    }

    @Test
    @DisplayName("returns the element when popped and is empty")
    void returnElementWhenPopped() {
        assertThat(stack.pop()).isEqualTo(anElement);
        assertThat(stack.isEmpty()).isTrue();
    }
}
  • 결과 : 요소가 1개 들어간 스택이 각각의 테스트 전에 만들어지므로 모든 테스트 통과
  • 테스트 : @BeforeAll 의 경우
    • 참고, @BeforeAll@AfterAll을 붙인 메소드는 static으로 선언되어한다. 그렇게 되면 클래스 내 지역변수를 사용하지 못하게 되므로 예시에서 인스턴스 변수를 사용할 수가 없다. 따라서 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 를 붙여줬다. 이에 대한 설명은 아래에. 이렇게 하면 @BeforeAll@AfterAll 도 static 메소드가 아닌 곳에도 붙일 수 있다.
@DisplayName("A stack")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class BeforeAllTest {
    Stack<Object> stack = new Stack<>();
    String anElement = "an element";

    @BeforeAll
    void createNewStack() {
        stack.push(anElement);
    }

    @Test
    @DisplayName("it is not empty")
    void isNotEmpty() {
        assertThat(stack.isEmpty()).isFalse();
    }

    @Test
    @DisplayName("returns the element when popped and is empty")
    void returnElementWhenPopped() {
        assertThat(stack.pop()).isEqualTo(anElement);
        assertThat(stack.isEmpty()).isTrue();
    }
}
  • 결과 : 첫번 째 실행된 테스트에서 pop을 해주었기 때문에 빈 stack의 형태에서 다시 두 번째 테스트를 했기 때문에 테스트 실패
  • @AfterEach @AfterAll 의 차이도 똑같다

6.2 어노테이션 2 : @TestInstance

  • 모든 테스트가 하나의 테스트 환경 내에서 실행되고자 할 때사용한다

    • (각각의 테스트 결과가 서로 영향을 준다)
  • JUnit은 기본적으로 각각의 test 메소드 실행 전에 test 클래스 인스턴스를 만든다. ("per-method" test instance lifecycle)

  • 그래서 인스턴스 변수를 선언해두고 각각의 테스트에서 이 변수 값을 변경해도 다른 테스트에는 영향을 미치치 않는다.

    • 테스트 :
    @DisplayName("A stack")
    public class LifecycleTest {
        Stack<Object> stack = new Stack<>();
        String anElement = "an element";
    
        @Test
        @DisplayName("push an element and is no longer empty")
        void isNotEmpty() {
            stack.push(anElement);
            assertThat(stack.isEmpty()).isFalse();
        }
    
        @Test
        @DisplayName("it is empty")
        void void isEmpty() { {
            stack.pop();
            assertThat(stack.isEmpty()).isTrue();
        }
    }
    • 결과 : 실패. 한 테스트에서 스택에 값을 넣어도 다른 테스트에 전혀 영향을 주지 않기 때문에 스택은 항상 비어있다. 따라서 isEmpty테스트는 pop할 값이 없기 때문에 실패한다.
  • 메소드끼리 영향을 주는 테스트를 만드려면 ( 하나의 공동의 테스트 환경 내에서 테스트를 실행하려면) @TestInstance 를 사용한다.

  • @TestInstance(Lifecycle.PER_CLASS)@TestInstance(Lifecycle.PER_METOD) 이렇게 두 가지로 사용할 수 있는데 후자는 디폴트이므로 굳이 써주지 않아도 된다.

  • 이렇게 @TestInstance(Lifecycle.PER_CLASS) 클래스 단위로 선언해주면 같은 테스트 환경을 메소드끼리 공유하게 된다.

    • 테스트 : 테스트를 통과하기 위해 @TestMethodOrder(MethodOrderer.OrderAnnotation.class) 로 테스트 순서를 지정해주었다.
    @DisplayName("A stack")
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    public class LifecycleTestPerClass {
        Stack<Object> stack = new Stack<>();
        String anElement = "an element";
    
        @Test
        @DisplayName("push an element and is no longer empty")
        @Order(1)
        void isNotEmpty() {
            stack.push(anElement);
            assertThat(stack.isEmpty()).isFalse();
        }
    
        @Test
        @DisplayName("it is empty")
        @Order(2)
        void isEmpty() {
            stack.pop();
            assertThat(stack.isEmpty()).isTrue();
        }
    }
    • 결과 : 같은 환경을 공유하므로 첫번째 테스트에서 요소를 하나 push 한 결과가 test2에도 반영되어 test2도 성공한다.

6.3. 어노테이션3 : @Disabled

  • 임시로 테스트를 비활성화 하게 해주는 어노테이션

    • 테스트 :
    public class ExampleTest {
        @Test
        @Disabled("잠시 테스트를 비활성화")
        void shouldShowSimpleWrongAssertion() {
            Assertions.assertEquals(1, 2);
        }
    
        @Test
        void shouldShowSimpleAssertion() {
            Assertions.assertEquals(1, 1);
        }
    }
    • 결과 : 테스트 통과 ( 잘못된 테스트를 비활성화 해 두어서)

6.4. 어노테이션4 : @DisplayName

  • 테스트에 설명을 붙일 수 있다.
    • 테스트 :
    @Test
        @DisplayName("Should demonstrate a simple assertion")
        void shouldShowSimpleAssertion() {
            Assertions.assertEquals(1, 1);
        }
    • 결과 : 왼쪽에서 보일 수 있는 것과 같이 테스트 설명이 그대로 나온다(어떤 테스트 실패, 성공을 알아보기 쉽다)

6.5 어노테이션5 : Data Driven Tests - @ParameterizedTest

  • AssertAll이 여러 assertions을 확인하는 방법이었다면
  • 같은 코드에 데이터를 여러개 바꿔가면서 테스트하고 싶다면 @ParameterizedTest를 이용하면 된다

6.5.1. 환경설정

  • @ParameterizedTest를 사용하고 싶다면 dependency에 testCompile 'org.junit.jupiter:junit-jupiter-params:5.7.0'을 추가해준다 (gradle의 경우)
  • 그리고 새로고침! ctrl + shift + o (dependecy 변경 사항 반영)
  • 출처 : Baeldung

6.5.2. 사용법과 예제

  • @ParameterizedTest 를 사용할 떄는 @Test를 사용하지 않는다
  • @ValueSource로 data를 array에 담아 전달할 수 있다. 그 외에 null과 빈 데이터소스를 전달하는 방식인 @NullSource, @EmptySource, @NullAndEmptySource가 있고 Enum을 활용해서 데이터를 전달하는 방식인 @EnumSource 가 있다. 팩토리 메서드를 사용해서 데이터를 전달하는 방식인 @MethodSource가 있다. 그 외에도 ,로 구분된 데이터 묶음을 전달하는 @CsvSource 등이 있으니 자세한 사항은 출처를 참고
  • @CsvSource 예시
@ParameterizedTest
@CsvSource({
    "apple,         1",
    "banana,        2",
    "'lemon, lime', 0xF1"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertNotEquals(0, rank);
}
  • 테스트 : @ValueSource로 값들이 홀수인지 확인하는 테스트를 해보았다.
@ParameterizedTest
    @DisplayName("Should check all numbers are odd numbers")
    @ValueSource(ints = {3, 4, 5, 8, 14})
    void shouldCheckOddNumbers(int number) {
        assertThat(number%2).isEqualTo(1);
    }
  • 결과 : 이렇게 에러와 함께 몇번째 데이터가 틀렸는지 알려준다.

6.6 어노테이션6 : @Nested

  • 특정 test 를 보다 큰 클래스로 묶어줄 수 있다. 테스트를 구조화 하는데 도움이 된다.
  • 테스트 : 참조자료에서 그대로 예시를 가져왔다. (조금 줄였다)
package bongf.junit5;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.Stack;

import static org.junit.jupiter.api.Assertions.*;

@DisplayName("A stack")
public class NestedTest {
    Stack<Object> stack;

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            @DisplayName("returns the element when popped and is empty")
            void returnElementWhenPopped() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }
        }
    }
}
  • 결과 : 구조화에 도움이 된다.

출처


이 페이지를 링크해둔 곳
profile
spring, java학습

0개의 댓글