개발/JAVA

[JAVA] 단위 테스트는 무엇이며, Junit5 에 대해 알아보자.

seopport 2023. 11. 29. 23:21
728x90
반응형

들어가기 전에

개발을 하면서 무엇보다도 중요한 점 중 하나는 안정적인 시스템을 구축하는 것입니다. 안정적인 시스템을 구축하기 위해서는 여러 테스트를 진행해야 하고, 테스트 케이스를 나누어 이상이 있는지 확인해야 합니다. 그러나 실무에서 긴급한 변경사항이나 예상하지 못한 버그는 개발자들을 괴롭힙니다. 때로는 시간에 쫓겨 테스트를 제대로 하지 못하고 구축하신 경험도 있으실 겁니다.

 

저는 SI 기업에서 프로젝트를 진행하신 경험이 있고, '기한일이 촉박하다' 등 여러 가지 이유로 단위 테스트를 진행하지 않고 통합 테스트를 진행한 경험이 있습니다. 심지어 기능 테스트로만 진행하신 분들도 있을 겁니다. 과연 운영 중에 문제가 없었을까요? 여러 서비스 기업에서는 TDD (테스트 주도 개발)을 진행 중이며, 단위 테스트를 통하여 안정성을 강화하고 코드의 질 향상 등 이미 여러 가지 노력을 하고 있습니다.

 

'단위테스트가 통합테스트보다 좋아?' 라고 물으신다면 질문 자체가 잘못되었다고 생각합니다. 단위테스트와 통합테스트는 상호보안적이며 두 가지의 테스트 모두 수행해야 좋은 서비스로 이어질 수 있다고 말씀드리고 싶습니다.

 

유닛테스트를 작성하는 것은 실제 기능을 구현하는 것보다 더 재미있다. 일단 맛을 들이고 나면 정말 그렇다. 유닛테스트 코드를 작성하는 것은 습관이 된 사람들은 유닛테스트의 옷을 입지 않은 벌거숭이 코드를 작성하는 것이 불안하게 느껴진다. 유닛테스트에 중독이 되었기 때문이다. 이 달콤한 중독의 맛을 아직 모르는 사람은 안타깝게도 프로그래밍의 맛을 반 밖에 모르는 사람이다.

- 프로그래밍은 상상이다 내용 중-

 

JUnit 5 logo
JUnit 5 logo

 

단위 테스트 란? 

소프트웨어 개발에서 가장 작은 단위의 코드인 함수, 메서드, 또는 클래스 등의 개별적인 모듈을 테스트하는 과정입니다. 이 테스트는 개별 컴포넌트가 의도한 대로 작동하는지를 확인하여 소프트웨어의 각 부분이 개별적으로 정확하게 동작하는지를 검증합니다.

 

간단한 예시를 들어보겠습니다. 우리가 만약 계산기를 만들어본다고 가정해 봅시다. 사칙연산 시 사용되는 +, -, *, % 의 기능을 개발해야 합니다. 단위테스트는 각 함수에 대한 테스트를 진행합니다.

 

단위 테스트 시나리오

1. 테스트 코드 작성

  • 개발자는 아직 구현되지 않은 기능 또는 모듈에 대한 테스트 코드를 작성합니다.
  • 이 테스트는 아직 실패할 것이고, 실제 코드를 작성하기 전에 실행될 수 있어야 합니다.

2. 실패하는 테스트 확인

  • 테스트를 실행하면 예상대로 실패할 것입니다. 왜냐하면 아직 해당 부분이 구현되지 않았기 때문입니다.

3. 코드 작성

  • 이제 개발자는 테스트를 통과시키기 위해 필요한 최소한의 코드를 작성합니다.
  • 목표는 테스트를 통과하는데 필요한 만큼의 코드를 작성하는 것입니다.

4. 테스트 통과

  • 작성한 코드가 테스트를 통과하면, 새로운 기능이나 모듈이 예상대로 작동하는지 확인할 수 있습니다.

5. 리팩토링

  • 테스트를 통과한 후에 코드를 리팩토링하여 코드의 가독성, 유지보수성을 향상시킵니다. 이때도 테스트가 계속 통과하는지 확인합니다

 

JUnit5

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

 

JUnit Platform

JVM 에서 테스트 프레임워크를 실행하는데 기초를 제공합니다. 또한 TestEngine API를 제공해 테스트 프레임워크를 개발할 수 있다.

 

JUnit Jupiter

JUnit 5에서 도입된 새로운 프로그래밍 모델로, 테스트 작성에 새로운 어노테이션과 기능을 제공합니다

 

JUnit Vintage

JUNit 3 및 JUNit 4 기반 테스트를 실행하기 위한 Test Engine을 제공합니다.

 

 

Supported Java Versions

JUnit 5는 실행 시 Java 8(또는 그 이상)이 필요합니다. 그러나 이전 버전의 JDK로 컴파일된 코드를 테스트할 수 있습니다.

 

Setting

Spring Boot 2.2 + 이상의 버전부터 JUnit5 가 기본적으로 의존성에 추가되기 때문에 별도의 세팅이 필요하지 않습니다.

testImplementation("org.springframework.boot:spring-boot-starter-test")
 
test {
    useJUnitPlatform()
}

 

Writing Tests

Junit Jupiter에서 테스트를 작성하기 위한 최소한의 요구사항을 보여줍니다.

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

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class MyFirstJUnitJupiterTests {

    private final Calculator calculator = new Calculator();

    @Test
    void addition() {
        assertEquals(2, calculator.add(1, 1));
    }

}

 

Annotations

Junit5 Annotation Description JUnit4 Annotation
@Test 메서드가 테스트 메서드임을 나타냅니다. @Test
@ParameterizedTest 메서드가 매개 변수가 있는 테스트임을 나타냅니다.  
@RepeatedTest 메서드가 반복 테스트를 위한 테스트 샘플릿임을 나타냅니다.  
@TestFactory 메서드가 동적 테스트를 위한 테스트 팩토리임을 나타냅니다.  
@TestTemplate 메서드는 등록된 공급자가 반환한 호출 컨텍스트 수에 따라 여러 번 호출되도록 설계된 테스트 사례의 템플릿임을 나타냅니다.  
@TestMethodOrder 테스트 메소드 실행 순서를 구성하는데 사용합니다.  
@DisplayName 테스트 클래스 또는 테스트 메서드에 대한 사용자 지정 표시 이름을 선언합니다.  
@DisplayNameGeneration 테스트 클래스에 대한 사용자 지정 표시 이름 생성기를 선언합니다. 이러한 주석은 상속됩니다.  
@BeforeEach 주석이 달린 메서드는 현재 클래스의 각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메서드 전에 실행되어야 함을 나타냅니다. @Before
@AfterEach 주석이 달린 메서드는 현재 클래스의 각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메서드 후에 실행되어야 함을 나타냅니다. @After
@BeforeAll 주석이 달린 메서드는 현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메서드 전에 실행되어야 함을 나타냅니다. @BeforeClass
@AfterAll 주석이 달린 메서드는 현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메서드 후에 실행되어야 함을 나타냅니다 @AfterClass
@Nested 주석이 달린 클래스가 비정적 중첩 테스트 클래스 임을 나타냅니다. Java 8 에서 Java 15 까지의 경우 "클래스별" 테스트 인스턴스 수명 주기를 사용하지 않는 한 @BeforeAll 및 @AfterAll 메서드를 @Nested 테스트 클래스에서 직접 사용할 수 없습니다. Java 16 부터 @BeforeAll 및 @AfterAll 메서드는 테스트 인스턴스 수명 주기 모드 중 하나를 사용하여 @Nested 테스트 클래스에서 static 으로 선언될 수 있습니다.  
@Tag 클래스 또는 메소드 레벨에서 태그를 선언할 때 사용합니다. (메이븐을 사용할 경우 설정에서 테스트를 태그로 인식해 포함하거나 제외시킬 수 있다.)  
@Disabled 테스트 클래스 또는 테스트 메서드를 비활성화하는 데 사용됩니다. @Ignore
@Timeout 실행이 저장된 기간을 초과하는 경우 테스트, 테스트 팩토리, 테스트 템플릿 또는 수명 주기 메서드를 실패하는 데 사용합니다.  
@ExtendWith 확장을 선언적으로 등록하는 데 사용합니다.  
@RegisterExtension 필드를 통해 프로그래밍 방식으로 확장을 등록할 때 사용합니다.  
@TempDir 수명 주기 메서드 또는 테스트 메서드에서 필드 주입 또는 매개변수 주입을 통해 임시 디렉토리를 제공하는데 사용합니다.
org.junit.jupiter.api.io 패키지에 있습니다.
 

 

 

Basic Annotations

새로운 어노테이션에 대해 논의하기 위해 우리는 섹션을 실행을 담당하는 그룹으로 나누었습니다.

  1. 테스트 전
  2. 테스트 중(선택 사항)
  3. 테스트 후

 

@BeforeAll and @BeforeEach

아래는 주요 테스트 케이스에 앞서 실행되는 간단한 코드의 예입니다.

@BeforeAll
static void setup() {
    log.info("@BeforeAll - executes once before all test methods in this class");
}

@BeforeEach
void init() {
    log.info("@BeforeEach - executes before each test method in this class");
}

 

@DisplayName and @ Disabled

다른 어노테이션 예시입니다.

@DisplayName("Single test successful")
@Test
void testSingleSuccessTest() {
    log.info("Success");
}

@Test
@Disabled("Not implemented yet")
void testShowSomething() {
}

 

@AfterEach and @AfterAll

아래는 주요 테스트 케이스 실행 후 작업에 관련된 코드의 예입니다.

@AfterEach
void tearDown() {
    log.info("@AfterEach - executed after each test method.");
}

@AfterAll
static void done() {
    log.info("@AfterAll - executed after all test methods.");
}

 

Assertions

테스트 조건을 설정하고 성공 여부 등을 판단하기 위한 유틸리티 메서드 모음입니다.

 

Assertions Method Description
asserEquals(expected, actual) 두 값이 같은지 확인합니다.
assertNotEquals(expected, actual) 두 값이 다른지 확인합니다.
assertTrue(condition) 조건이 참인지 확인합니다.
assertFalse(condition) 조건이 거짓인지 확인합니다.
assertNull(actual) 값이 Null 인지 확인합니다.
assertNotNull(actual) 값이 Null 이 아닌지 확인합니다.
assertArrayEquals(expectedArray, actualArray) 배열 간의 일치 여부를 확인합니다.
assertIterableEquals(expected, actual) 객체 간의 일치 여부를 확인합니다.
assertLinesMatch(expected, actual) 두 문자열 목록이 일치하는지 확인합니다.
assertNotEquals(expected, actual, messageSupplier) 메세지 포함하여 값이 다른지 확인합니다.
assertThrows(expectedType, executable) 예외가 발생하는지 확인하고 해당 예외의 타입이 기대 타입과 일치하는지 확인합니다.
assertDoesNotThrow(executable) 예외가 발생하지 않는지 확인합니다.
assertAll(executables...) 모든 실행 가능한 코드 블록을 실행하고, 각각의 결과를 확인합니다.
assertTimeout(duration, executable) 특정 시간 내에 실행이 완료되는지 확인합니다.

 

@assertTrue + lambda

@Test
void lambdaExpressions() {
    List numbers = Arrays.asList(1, 2, 3);
    assertTrue(numbers.stream()
      .mapToInt(Integer::intValue)
      .sum() > 5, () -> "Sum should be greater than 5");
}

 

assertTrue 검증과 JAVA8 부터 지원하는 lambda를 활용하여 테스트 코드를 작성하는 시간과 비용을 줄일 수 있습니다.

 

@assertAll, @assertEqulas

 @Test
 void groupAssertions() {
     int[] numbers = {0, 1, 2, 3, 4};
     assertAll("numbers",
         () -> assertEquals(numbers[0], 1),
         () -> assertEquals(numbers[3], 3),
         () -> assertEquals(numbers[4], 1)
     );
 }

 

asarcetAll()를 사용하여 assert 메서드 들을 그룹화할 수도 있습니다. 또한 그룹 내에서 실패한 내용을 Multiple Failures Error로 보고할 수 있습니다. 즉 복잡한 검증을 할 수 있고 오류 파악을 할 때 정확한 위치를 알 수 있다는 것입니다.

 

Assumptions

Assumptions 은 특정 조건이 충족된 경우에만 테스트를 실행하는 데 사용됩니다. 

 

Assumption with assumeTrue(), assumeFalse(), and assumingThat()

@Test
void trueAssumption() {
    assumeTrue(5 > 1);
    assertEquals(5 + 2, 7);
}

@Test
void falseAssumption() {
    assumeFalse(5 < 1);
    assertEquals(5 + 2, 7);
}

@Test
void assumptionThat() {
    String someString = "Just a string";
    assumingThat(
        someString.equals("Just a string"),
        () -> assertEquals(2 + 2, 4)
    );
}

 

환경에 따라서 테스트할 수 있도록 도움을 주는 곳에 사용하기도 합니다.

public class ExampleTest {

    @Test
    public void testInDevEnvironment() {
        // ASSUME_TRUE: 특정 조건이 참일 때만 테스트 실행
        Assumptions.assumeTrue("dev".equals(System.getProperty("ENVIRONMENT")));
        System.out.println("개발 서버입니다.");
    }

    @Test
    public void testNonDevEnvironment() {
        // ASSUME_FALSE: 특정 조건이 거짓일 때만 테스트 실행
        Assumptions.assumeFalse("dev".equals(System.getProperty("ENVIRONMENT")));
        System.out.println("개발 서버가 아닙니다.");
    }
}

 

Exception Testing

Juint 5에는 두 가지 예외 테스트 방법이 있으며, 이 두 가지 방법은 모두 asertThrows() 방법을 사용하여 구현할 수 있습니다.

 

@Test
void shouldThrowException() {
    Throwable exception = assertThrows(UnsupportedOperationException.class, () -> {
      throw new UnsupportedOperationException("Not supported");
    });
    assertEquals("Not supported", exception.getMessage());
}

@Test
void assertThrowsException() {
    String str = null;
    assertThrows(IllegalArgumentException.class, () -> {
      Integer.valueOf(str);
    });
}

 

첫 번째 예는 입력된 예외의 세부 정보를 확인하고 두 번째 예제는 예외 유형을 확인합니다.

 

 

 

Test Suites

테스트 클래스들을 그룹화하고 실행하기 위해 다양한 어노테이션을 사용합니다. 그중에서 @SelectPackages, @SelectClasses, @IncludeTags, @IncludePackages, @IncludeClassNamePatterns 등이 있습니다.

 

JUnit 5 Test Suites

 

JUnit 5 Test Suites with Examples - HowToDoInJava

Unit 5 test suites are written with @Suite annotation. Suites help us run the tests spread into multiple classes and packages.

howtodoinjava.com

 

Test Suites Code

// 생명 주기를
@TestInstance(Lifecycle.PER_CLASS)
public class TestSuiteExample {

    // 여러 테스트 클래스를 그룹화하고 실행하는 어노테이션
    // @SelectPackages 에 특정 클래스 여러 개 설정 가능
    @Test
    @DisplayName("Run Test Suites")
    @Tag("Suite")
    @SelectPackages("com.example.tests")
    void runTestSuites() {
        // "com.example.tests" 패키지 안의 모든 테스트 클래스가 실행됩니다.
    }
    // 테스트 코드 작성
}

 

Dynamic Test

JUnit 5의 Dynamic Test는 런타임에 동적으로 테스트를 생성할 수 있는 기능을 제공합니다. 정적인 방식으로 테스트를 작성하는 것이 아니라 프로그래밍적으로 동적으로 테스트 케이스를 생성할 수 있습니다.

 

동적 테스트는 @TestFactory로 사용해야만 합니다.

@TestFactory
Stream<DynamicTest> translateDynamicTestsFromStream() {
    return in.stream()
      .map(word ->
          DynamicTest.dynamicTest("Test translate " + word, () -> {
            int id = in.indexOf(word);
            assertEquals(out.get(id), translate(word));
          })
    );
}

 

Return을 Stream, Collection, Itable 또는 Iterator을 반환해야 합니다.

(The factory method must return a Stream, Collection, Iterable, or Iterator.)

 

@TestFactory 메서드는 private OR static 이면 안됩니다.

(Please note that @TestFactory methods must not be private or static.)

 

테스트 횟수는 동적이며, ArrayList 사이즈에 따라 다릅니다.

(The number of tests is dynamic, and it depends on the ArrayList size)

 

참고 페이지

Junit5 - 공식 문서

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model do 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 custo

junit.org

 

민동현 Dev - JUnit5 완벽 가이드

 

JUnit5 완벽 가이드

시작하기전

donghyeon.dev

 

좋아질꺼야 블로그

 

[JUnit] JUnit5 기초지식 정리( 설정, annotation, assertions )

java 개발자가 가장 많이 사용하는 테스트 프레임워크 JUnit에 대한 기초지식 많은 개발 방법론이 있지만 TDD의 핵심인 Test Code 작성을 위한 프레임워크 중 java 개발자들이 가장 많이 이용하는 프레

kangyb.tistory.com

 

Ruby on Software

 

JUnit 5 공식 가이드 문서 정리

JUnit 5 공식 가이드 문서 정리

velog.io

 

728x90
반응형