테스트

Junit 클래스

sumni0530 2022. 7. 30. 12:30

개발 이후에 개발이 제대로 되었는지 검증하기 위해 Test 코드를 작성한다.

Test의 가장 작은 단위인 Unit 테스트는 개발한 기능이 제대로 동작하는지 가장 작은 기능 단위로 테스트하는 것을 의미한다.

Test 코드는 원하는 기능이 제대로 동작하는지 검증하는 것과 다양한 이해관계자와 소통하기 위해 사용된다.

테스트는 자동화하기 어렵고, 각 개별 테스트 케이스 별 옳고/그름이 판단하기 어렵기 때문에 다양한 테스트 프레임워크를 사용할 수 있으며, Junit 프레임워크에 대해 알아보자.

 

 

Junit 프레임워크

테스트 라이프 사이클을 갖고 있으며, 이를 제어하기 위해 메서드 에너테이션을 사용한다.

테스트 라이프 사이클

  • @BeforeEach : 각각의 테스트 메서드 전에 수행되는 메서드 (set-up 코드(객체 생성, 테스트 데이터 생성)에 사용)
  • @AfterEach : 각각의 테스트 메서드 후에 수행되는 메서드 (clean-up 코드(리소스 해제, 테스트 데이터 삭제)에 사용)
  • @BeforeAll : 모든 테스트 메서드 시작 전에 한번만 실행되는 메서드 (데이터베이스 및 서버 연결에 사용)
  • @AfterAll :  모든 테스트 메서드 실행 후에 한번만 실행되는 메서드 (데이터베이스 및 서버 연결 해제에 사용)

 

Junit에서는 각 테스트 케이스의 이름을 설정하는 에너테이션도 지원한다.

  • @DisplayName : 테스트 메서드에 대한 다양한 표현 방식(한글, 숫자, 띄어쓰기)을 제공하기 위해 사용
  • @DisplayNameGeneration : 테스트 메서드를 기반으로 메서드이름을 자동 변경해주는 메서드 에너테이션
    • @DisplayNameGeneration(DisplayNameGenerator.Simple.class) : 테스트 메서드의 괄호를 제거
    • @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) : 테스트 메서드의 언더스코어(_)를 스페이스로( )로 변경
    • @DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class) : <테스트 클래스>, <테스트 메서드> 로 표시 

Order test

Unit 테스트의 특성상 각 테스트 간의 종속성이 없어야되기 때문에 순서가 의미가 없어야한다. Junit 에서 order 를 사용하여 매번 동일한 순서대로 테스트 메서드를 실행하지만 내부로직으로 결정한다. (유의미한 순서는 아님)
순서를 지정하는 에너테이션(@TestMethodOrder)을 사용하여 기능 별 그룹화 / 다른 사람과의 테스트 케이스 공유 / 테스트 케이스 정렬 등을 수행할 수 있다.

  • @TestMethodOrder(MethodOrderer.DisplayName.class) - @DisplayName의 알파벳 순서에 맞게 정렬
  • @TestMethodOrder(MethodOrderer.MethodName.class) - 테스트 메서드의 알파벳 순서에 맞게 정렬
  • @TestMethodOrder(MethodOrderer.Random.class) - 메서드를 랜덤하게 실행하며, 테스트 케이스 간의 의존성이 제거됨을 확인하기에 좋음
  • @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - 메서드를 랜덤하게 실행하며, 테스트 케이스 간의 의존성이 제거됨을 확인하기에 좋음
    • @Order(숫자) 에너테이션을 사용하여 명시적으로 순서를 지정할 수 있으며, 숫자가 낮을수록 우선순위가 높음 

 

테스트 케이스 필터링

모든 테스트 케이스가 항상 동작해야되는 것은 아니다. 예를 들면 특정 OS에서만 사용하거나, 개발계 운영계 테스트가 다를 수 있다. 이를 위해 테스트 케이스별로 활성화/비활성화를 지정할 수 있다.

  • @Disabled - 테스트 케이스 비활성화를 위한 에너테이션
  • @EnabledOnOS - 특정 OS에서만 동작하기 위한 에너테이션, 다중 값 사용시 {} 로 묶음
    ex. @EnabledOnOs({OS.MAC, OS.WINDOWS})
  • @EnabledOnJre - 특정 JAVA 버전에서 테스트 수행 
    ex. @EnabledOnJre(JRE.JAVA_11)
  • @EnabledForJreRange - 자바 버전의 범위를 지정하여 테스트 수행
    ex. @EnabledForJreRange(min=JRE.JAVA_11,max = JRE.JAVA_17)
  • @EnabledIfEnvironmentVariable - 특정 환경변수가 설정되었을 때 테스트 케이스 실행 (환경변수 설정 방법은 캡처로 같이 확인 아래쪽 RUN 탭의 스패너 모양)

 

Junit 프레임워크에는 다양한 클래스가 포함되며 주로 사용될 클래스는 Assertion 클래스이다.

다양한 메서드가 포함되어 있기 때문에 *(와일드 카드)를 사용하여 static import 후 메서드를 사용할 수 있으며, 이후 메서드명만 호출하여 사용 가능하다.(import static org.junit.jupiter.api.Assertions.*)

테스트 코드에는 @Test 에너테이션을 붙여서 테스트 코드임을 명시한다. 테스트 코드는 given(초기 값 세팅), when(테스트 기능 수행), then(테스트 통과 기준) 으로 이루어진다. 

Method 설명
assertEquals(<기대되는 값>, <실제 값>) 기대값과 실제 값의 일치여부 확인
assertNotEquals(<기대되는지 않는 값>, <실제 값>) 기대하지 않은 값과 실제 값의 불일치여부 확인
assertNull(<객체>) 인자 값이 Null 인지 확인
assertNotNull(<객체>) 인자 값이 Null이 아닌지 확인
assertSame(<객체1>,<객체2>) 인자의 객체가 동일한지 확인
assertNotSame(<객체1>,<객체2>) 인자의 객체가 동일하지 않은지 확인
assertTrue(<조건식 or Boolean 타입 객체>) 인자의 조건이 참인지 확인
assertFalse(<조건식 or Boolean 타입 객체>) 인자의 조건이 거짓인지 확인
assertArrayEquals(<array 타입 객채1>,<array 타입 객채2>) 두 객체 배열이 동일한지 확인
assertIterableEquals(<iterable 타입 객체1>,<iterable 타입 객체2>) 두 iterable 객체가 동일한지 확인
assertLineMatch(<List 타입 객체1>,<List 타입 객체2>) 문자열 리스트의 일치 여부를 확인
assertThrows(<기대되는 예외 클래스>, <람다식>) 기대되는 타입의 예외를 발생시키는지 확인
assertDoesNotThrow(<기대되는지 않는 예외 클래스>, <람다식>) 기대되지 않은 타입의 예외를 발생시키지 않은지 확인
assertTimeoutPreemptively(<Duration 객체 시간>, <람다식>) 주어진 시간 이전에 메서드를 끝나는지 확인

위의 메서드의 사용 코드는 아래와 같다.

import org.junit.jupiter.api.*;

import java.time.Duration;
import java.util.List;

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

class DemoUtilsTest {
    @Test
    @DisplayName("Equals and Not Equals")
    void testEqualsAndNotEquals() {
        // given
        int expected = 6;
        int unexpected = 8;

        // when
        int actual = demoUtils.add(2, 4);

        // then
        assertEquals(expected, actual, "2+4 must be 6");
        assertNotEquals(unexpected, actual, "2+4 must not be 8");
    }
    
    @Test
    @DisplayName("Null and Not Null")
    void testNullAndNotNull() {
        //given
        String str1 = null;
        String str2 = "luv2code";

        // when & then
        assertNull(demoUtils.checkNull(str1), "Object should be null");
        assertNotNull(demoUtils.checkNull(str2), "Object should not be null");
    }

    @Test
    @DisplayName("Same and Not Same")
    void testSameAndNotSame() {
        // given
        String str = "luv2code";

        // when & then
        assertSame(demoUtils.getAcademy(), demoUtils.getAcademyDuplicate(), "Object should refer to same object");
        assertNotSame(str, demoUtils.getAcademy(), "Object should not refer to same object");
    }
    
    @Test
    @DisplayName("True and False")
    void testTrueFalse() {
        // given
        int gradeOne = 10;
        int gradeTwo = 5;

        // when & then
        assertTrue(demoUtils.isGreater(gradeOne, gradeTwo), "This should return true");
        assertFalse(demoUtils.isGreater(gradeTwo, gradeOne), "This should return false");
    }
    
    @Test
    @DisplayName("Array Equals")
    void testArrayEquals() {
        // given
        String[] stringArray = {"A", "B", "C"};
        
        // when & then
        assertArrayEquals(stringArray, demoUtils.getFirstThreeLettersOfAlphabet(), "Arrays should be the same");
    }
    
    @Test
    @DisplayName("Iterable equals")
    void testIterableEquals() {
        // given
        List<String> theList = List.of("luv", "2", "code");

        // when & then
        assertIterableEquals(theList, demoUtils.getAcademyInList(), "Expected list should be same as actual list");
    }
    
    @Test
    @DisplayName("Lines match")
    void testLineMatch() {
        // given
        List<String> theList = List.of("luv", "2", "code");
        
        // when & then
        assertLinesMatch(theList, demoUtils.getAcademyInList(), "Lines should match");
    }
    
    @Test
    @DisplayName("Throws and Does Not Throws")
    void testThrowsANdDoesNotThrow() {
        // given & when & then
        assertThrows(Exception.class, () -> {
            demoUtils.throwException(-1);
        }, "Should throw exception");

        assertDoesNotThrow(() -> {
            demoUtils.throwException(1);
        }, "Should not throw exception");
    }
    
    @Test
    @DisplayName("Timeout")
    void testTimeout() {
        // given & when & then
        assertTimeoutPreemptively(Duration.ofSeconds(3), () -> {
            demoUtils.checkTimeout();
        }, "Method should execute in 3 seconds");
    }
}

 

 

테스트하고자 하는 기능을 얼마나 잘 테스트하는지 평가하기 위해 코드 커버지리를 사용할 수 있다. 

코드 커버리지는 얼마나 많은 메서드/라인이 테스트에서 호출되고 사용되는지 측정한 값으로, 이상한 테스트 코드로 값을 높일 수 있기 때문에 코드 커버리지의 값이 높을수록 좋은 테스트 코드라는 것은 보장할 수 없다. 

IntelliJ에서는 코드 커버리지를 확인하기 위한 기능을 제공하고 있다. [Run <테스트 클래스 명> with Coverage] 를 통해 실행하면 자동으로 코드 커버리지를 계산해준다. 클래스/메서드/라인 별로 평가 결과를 제공하며 실무에서는 일반적으로 70~80% 정도되면 인용 가능한 수준이라고 한다.