JUnit

김소희·2023년 7월 24일
2

JUnit은 자바 언어를 위한 유닛 테스트 프레임워크로, 자바 애플리케이션에서 테스트 코드를 작성하고 실행하는 데 사용된다. 코드의 작은 단위인 함수와 메소드 별로 테스트케이스를 작성하는 단위테스트에 사용되며, 내가 의도한 대로 동작하는지 단정문(Assert)으로 기대값에 대한 수행결과를 확인 할 수 있다.

JUnit 5의 모듈

Spring Boot 2.2 버전부터는 JUnit 5 버전을 사용하는데 JUnit 5는 JUnit Platform, Jupiter, Vintage 세 가지 주요 모듈로 구성되어있다.

  • JUnit Platform : 다른 테스트 프레임워크를 JUnit 5의 테스트 엔진으로 실행할 수 있는 확장 가능한 테스트 플랫폼을 제공한다.
  • Jupiter : JUnit5의 구현체로 주요 기능을 담당하는 모듈이다.
  • Vintage : 하위 호환성을 위한 JUnit3, JUnit4의 구현체이다.

JUnuit5 Test life Cycle Annotation

AnnotationDescription
@Test테스트용 메소드를 표현하는 어노테이션
@BeforeEach각 테스트 메소드가 시작되기 전에 실행되어야 하는 메소드를 표현
@AfterEach각 테스트 메소드가 시작된 후 실행되어야 하는 메소드를 표현
@BeforeAll테스트 시작전에 시작되어야 하는 메소드를 표현 (static 처리 필요)
@AfterAll테스트 종료후에 시작되어야 하는 메소드를 표현 (static 처리 필요)

JUnuit5 Main Annotation

AnnotationDescription
@SpringBootTest통합테스트 용도로 사용됨
@SpringBootApplication을 찾아가 하위의 모든 Bean을 스캔하여 로드함
그 후 Test용 Application Context를 만들어 Bean을 추가하고, MockBean을 찾아 교체
@ExtendWith테스트 실행을 확장하기 위해 사용됨
JUnit 4에서 @RunWith 로 사용되던 애너테이션이 ExtendWith로 변경됨
@WebMvcTest컨트롤러와 연관된 Bean이 모두 로드됨
@SpringBootTest대신 컨트롤러 관련 코드만 테스트 할 경우 사용
@Autowired about MockbeanController의 API를 테스트하는 용도인 MockMvc 객체를 주입 받음
perform() 메소드를 활용하여 컨트롤러의 동작을 확인할 수 있음
.andExpect(),andDo(), andReturn()등의 메소드를 같이 활용함
@MockBean의존성이 있는 경우 가짜 객체를 생성해주는 어노테이션
given() 메소드를 활용하여 가짜객체의 동작에 대해 정의하여 사용할 수 있음
@AutoConfigureMockMvcMockMvc의 의존성을 자동으로 주입
@Import필요한 class 들을 Configuration으로 만들어서 사용할 수 있음

F.I.R.S.T 원칙

단위테스트는 단위가 최대한 작아야 하고 빠르게 실행되고 빠르게 결과를 알아야 한다. 또한 실제 서버나 데이터베이스를 거치지 않고 가짜 데이터를 만들어서 테스트를 진행한다. 단위테스트는 독립적이여야 하고 다른테스트에 의존하거나 영향을 주어선 안된다. 그리고 반복 가능해야하고 몇번을 진행하든 똑같은 결과가 나와야 한다. 결과는 성공이나 실패로 분명하게 알 수 있어야하고 TDD원칙에 따라 시기적절한 때에 작성되어야 한다.

원칙내용
Fast테스트 코드의 실행은 빠르게 진행되어야 함
Independent독립적인 테스트가 가능해야 함
Repeatable테스트는 반복 가능하고 매번 같은 결과를 만들어야 함
Self-Validation테스트는 그 자체로 실행하여 결과를 검증할 수 있어야 함
Timely단위테스트는 비즈니스 코드가 완성되기 에 구성하고 테스트가 가능해야 함

Assertion 메서드

Assertion은 ‘예상하는 결과 값이 참(true)이길 바라는 논리적인 표현’이다.
JUnit에서는 Assertion과 관련된 다양한 메서드를 사용해서 테스트 대상에 대한 Assertion을 진행할 수 있다.

  • assertEquals()
    : 기대하는 값과 실제 결과 값이 같은지를 검증할 수 있다
    : 기대하는 문자열(expected)과 실제 결과 값(autual)이 일치하는지를 검증
  • assertNotNull()
    : 테스트 대상 객체가 null 이 아닌지를 테스트 한다.
    : Null 여부 테스트
  • assertThrows()
    : 호출한 메서드의 동작 과정 중에 예외가 발생하는지 테스트
    : 예외(Exception) 테스트
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.CountDownLatch;

import example.domain.Person;
import example.util.Calculator;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

class AssertionsDemo {

    private final Calculator calculator = new Calculator();

    private final Person person = new Person("Jane", "Doe");

    @Test
    void standardAssertions() {
        assertEquals(2, calculator.add(1, 1));
        assertEquals(4, calculator.multiply(2, 2),
                "The optional failure message is now the last parameter");
        assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and all
        // failures will be reported together.
        assertAll("person",
            () -> assertEquals("Jane", person.getFirstName()),
            () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        // Within a code block, if an assertion fails the
        // subsequent code in the same block will be skipped.
        assertAll("properties",
            () -> {
                String firstName = person.getFirstName();
                assertNotNull(firstName);

                // Executed only if the previous assertion is valid.
                assertAll("first name",
                    () -> assertTrue(firstName.startsWith("J")),
                    () -> assertTrue(firstName.endsWith("e"))
                );
            },
            () -> {
                // Grouped assertion, so processed independently
                // of results of first name assertions.
                String lastName = person.getLastName();
                assertNotNull(lastName);

                // Executed only if the previous assertion is valid.
                assertAll("last name",
                    () -> assertTrue(lastName.startsWith("D")),
                    () -> assertTrue(lastName.endsWith("e"))
                );
            }
        );
    }

    @Test
    void exceptionTesting() {
        Exception exception = assertThrows(ArithmeticException.class, () ->
            calculator.divide(1, 0));
        assertEquals("/ by zero", exception.getMessage());
    }

    @Test
    void timeoutNotExceeded() {
        // The following assertion succeeds.
        assertTimeout(ofMinutes(2), () -> {
            // Perform task that takes less than 2 minutes.
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        // The following assertion succeeds, and returns the supplied object.
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod() {
        // The following assertion invokes a method reference and returns an object.
        String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
        assertEquals("Hello, World!", actualGreeting);
    }

    @Test
    void timeoutExceeded() {
        // The following assertion fails with an error message similar to:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            new CountDownLatch(1).await();
        });
    }

    private static String greeting() {
        return "Hello, World!";
    }

}

Assumptions 메서드

Assumption은 ‘~라고 가정하고’라는 표현을 쓸 때의 ‘가정’에 해당한다.
JUnit 5의 Assumption 기능을 사용하면 특정 환경에만 테스트 케이스가 실행되도록 할 수 있다.

  • assumeTrue()
    : 파라미터로 입력된 값이 true이면 나머지 아래 로직들을 실행한다.
  • assumingThat()
    : 첫 번째 인자의 값이 true이면, 두 번째 인자로 받은 검증을 수행한다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class AssumptionsDemo {

    private final Calculator calculator = new Calculator();

    @Test
    void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));
        // remainder of test
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "Aborting test: not on developer workstation");
        // remainder of test
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // perform these assertions only on the CI server
                assertEquals(2, calculator.divide(4, 2));
            });

        // perform these assertions in all environments
        assertEquals(42, calculator.multiply(6, 7));
    }

}
profile
백엔드 자바 개발자 소희의 노트

0개의 댓글