JAVA/JUnit

JUnit 5, JAVA 테스트 코드 작성하기

수달하나 2023. 3. 15. 10:14

실무 개발자 질문

예전에 취업준비생 이었을 때 마지막 최종 면접을 보면서 받은 질문이 있다.

"JUnit 5 를 사용하셨던데 왜 JUnit 5 를 이용하셨고, 테스트 케이스를 작성하신 이유가 있을까요?"

→ "테스트를 할 때 JUnit 5 를 많이 사용해서 진행한다고 해서 사용했고 특별한 이유는 없었습니다."

여기 까지의 답변만 봐도 탈락이지만 좀 더 질문을 받았다.

"그럼 왜 테스트 케이스를 만들어서 사용하는 걸 까요?, 굳이 작성 할 필요가 있을 까요?"

잘 기억이 나지 않지만 이리저리 짱구를 돌리다가 내 놓은 대답은 그냥 얼버무린 대답이었다. 나는 테스트 케이스를 왜 작성해야 하는지도 모른체, JUnit 5 을 이용해서 테스트 케이스를 작성했다.  


테스트 케이스는 왜 작성 할 까?

 

이 질문에 대해서 명확하게 대답할 수 있는 사람이 있을까? 개발자가 되기전에는 많은 사람들이 사용하는 것에 있어서, 그리고 그렇게 하는 것에 있어서 정해진 답이 있을 것이라고 생각했다. 그리고 그것이 곧 정답이라고 생각했다. 그런데 실제로 개발을 하다 보면 이해가 안 될 때도 많고 이런 이유때문이라면 굳이 사용할 필요가 없지 않을까? 라는 생각을 할 때가 있다. 예를 들어서 Java 에서 제공하는 함수형 프로그램중에 Stream 기능을 가독성 때문에 많이들 사용한다고 하지만 난 굳이 사용 하지 않는다. 속도면에서 큰 차이가 있는것은 아니지만 엄밀하게 따지자면 Stream 이 더 느린 속도를 보여주기도 하고 가독성 면에서도 엄청난 장점이 있다고 생각하지 않기 때문에다. 처음으로 돌아와서 테스트 케이스를 왜 작성하냐는 질문에 테스트 케이스를 사용해서 정상 작동하는지 확인하기 위해서 테스트 케이스를 작성한다 는 기본적인 얘기 말고 내가 테스트 케이스를 작성하는 진짜 이유가 필요하다는 것이다. 만일 테스트 케이스가 정말 필요하지 않다고 생각한다면 그것또한 그 개발자가 개발을 하는 방식이라고 생각한다. 정답은 없는거니깐. 하지만 나는 테스트 케이스를 작성하는것이 전체 프로그램을 설계하는데 도움이 될 수 있다고 생각한다. 최근에 좋은 프로그램은 무엇일까 라는 생각을 하다가 내린 결론은 테스트 하기 좋은 프로그램이다 라는 생각을 했다. 테스트가 편하다는 것은 모듈화가 잘 되어있다는 것 그리고 곧 구조적으로 잘 설계되어있다는 프로그램을 의미 하기 때문이다. 물론 잘 설계된 프로그램의 큰 이유는 아니더라도 어쨌든 구조적으로 잘 설계된 프로그램이라면 테스트 케이스를 작성하는데 어려움이 없기 때문이다. 따라서 테스트 케이스를 작성하면서 구조적으로 잘 설계 되었는지 까지 판단할 수 있다면 테스트 케이스를 작성 할 이유가 충분하다고 생각한다.


JUnit 5

 

JUnit 5 를 사용하면서 혹은 사용 할 때 주의해야 할 점들이 몇가지 있어서 그 부분들을 살펴 보면 좋을 것 같다. 실제로 코드를 작성하다 보면 왜 이렇게 사용 해야 하고 어떻게 사용해야 하는지 잘 모를 때가 많다. 

 

접근 제한자

JUnit 5 로 오면서 항상 public 을 유지해야 했던 테스트 클래스의 규칙이 깨지면서 제한이 풀렸다. 따라서 테스트 클래스를 굳이 public 으로 유지 할 필요가 없다. (기존의 public 고수방법은 기존의 JUnit 의 전통을 이어오기 위해서 그렇게 정의 했다고 한다. ?! ) 하지만 class 에 대한 접근제한자의 규칙이 깨졌을 뿐 메소드 에선 protected 와 private 를 사용 할 수 없다. public 으로 사용 하던가 혹은 같은 패키지 내부에서만 사용 할 수 있는 default 로 정의 하고 사용해야 한다.

 

RollBack 

테스트 메소드를 작성하고 DB 에 저장되는 로직을 타면 실제로 DB에 반영이 된다. 따라서 DB에 반영되지 않는 테스트 메서드를 만드려면 테스트가 끝남과 동시에 Rollback 을 할 수있도록 @Rollback 어노테이션을 붙여야 한다.

 

공통 처리

테스트 메서드에 관해서 공통 처리 할 부분이 있다면 혹은 모든 테스트 메서드가 실행되기 전이나 후에 반드시 해야 하는 작업이 있다면 @BeforeEach, @AfterEach, @BeforeAll, @AfterAll 어노테이션을 통해서 정의 할 수 있다.

 

테스트 클래스 위치

기본적으로 빌드시 @Test 어노테이션이 있는 메서드의 경우 테스트 메서드로 인식이 되어도 빌드 된다.(설정을 통해 빌드포함 여부를 결정 할 수 있지만 기본적으로는 빌드 되는것이 맞음) 하지만 Jar 파일이나 War 파일로 배포 할 경우 실제 프로덕션 코드에 포함되지 않기 때문에 테스트 코드가 포함이 되지 않는다. 따라서 새로운 패키지에 분리하여 테스트 클래스를 작성하고 테스트용 클래스를 따로 배포 하거나 하는 식의 관리 방법을 사용 하는것이 일반적이다. 다른 디렉토리에 위치하여 빌드를 제외 하고 별개로 빌드 되도록 할 경우에는 빌드 시간을 단축 시킬 수 있다는 장점도 있다.


JUnit 에서 사용하는 대표적인 어노테이션

  1. @Test: 해당 메소드가 테스트 메소드임을 지정합니다.
  2. @BeforeAll: 해당 클래스의 모든 테스트 전에 딱 한 번 실행됩니다.
  3. @BeforeEach: 각각의 테스트 메소드가 실행되기 전에 실행됩니다.
  4. @AfterEach: 각각의 테스트 메소드가 실행된 후에 실행됩니다.
  5. @AfterAll: 해당 클래스의 모든 테스트가 실행된 후에 딱 한 번 실행됩니다.
  6. @DisplayName: 해당 테스트 메소드의 이름을 변경하여 더 의미 있는 이름으로 표시할 수 있습니다.
  7. @Disabled: 해당 테스트 메소드를 비활성화시킬 수 있습니다.
  8. @Timeout: 해당 테스트 메소드가 지정된 시간 내에 완료되지 않으면 실패 처리됩니다.
  9. @ParameterizedTest: 매개 변수를 전달하여 같은 코드로 다양한 입력값을 테스트할 수 있습니다.
  10. @RepeatedTest: 동일한 테스트를 여러 번 반복하여 실행할 수 있습니다.
class MyTestClass {

    @BeforeAll
    static void beforeAll() {
        // 테스트 클래스의 모든 테스트 메소드 실행 전에 딱 한번 실행됨
    }

    @BeforeEach
    void beforeEach() {
        // 테스트 메소드 실행 전에 실행됨
    }

    @Test
    void testMethod() {
        // 테스트 메소드
    }

    @Test
    @Disabled("이 테스트는 아직 구현되지 않았습니다.")
    void disabledTestMethod() {
        // 비활성화된 테스트 메소드
    }

    @AfterEach
    void afterEach() {
        // 테스트 메소드 실행 후에 실행됨
    }

    @AfterAll
    static void afterAll() {
        // 테스트 클래스의 모든 테스트 메소드 실행 후에 딱 한번 실행됨
    }
    
    @RepeatedTest(5) // 5번 반복 실행
    void testRepeated() {
        int num = (int) (Math.random() * 100);
        assertTrue(num < 100);
    }

    @Test
    @Timeout(5) // 5초 이내에 실행되지 않으면 실패 처리
    void testMethodWithTimeout() {
        // 특정 시간 내에 실행되어야 하는 테스트 메소드
    }

    @Test
    void assertTestMethod() {
        String str1 = "JUnit";
        String str2 = "JUnit";
        Assertions.assertEquals(str1, str2); // 두 값이 같은지 검사하는 메소드
    }

    @Test
    void assertExceptionTestMethod() {
        String str = null;
        Assertions.assertThrows(NullPointerException.class, () -> {
            str.length(); // NullPointerException 예외가 발생하는지 검사하는 메소드
        });
    }

    @Test
    void assertAllTestMethod() {
        int a = 2;
        int b = 3;
        int c = 4;
        Assertions.assertAll("numbers",
                () -> Assertions.assertEquals(a, 2),
                () -> Assertions.assertEquals(b, 3),
                () -> Assertions.assertEquals(c, 4)
        ); // 여러 검사를 하나로 묶어서 검사하는 메소드
    }
}
@DisplayName("계산기 테스트")
class CalculatorTest {

    @Test
    @DisplayName("덧셈 테스트")
    void testAdd() {
        // 덧셈 테스트 수행
    }

    @Test
    @DisplayName("뺄셈 테스트")
    void testSubtract() {
        // 뺄셈 테스트 수행
    }
}