[테스트 주도 개발 시작하기] CHAPTER 5 - JUnit 5 기초

myeonji·2023년 1월 5일
0
  • JUnit 5 모듈 구성
  • 테스트 메서드 작성
  • 주요 단언 메서드
  • 라이프사이클

지금까지 JUnit이 제공하는 @Test 애노테이션을 이용해서 테스트 메서드를 작성하고 assertEquals() 메서드를 이용해서 값을 비교했다.

이외에 추가적으로 몇 가지 더 알아야 JUnit을 잘 사용할 수 있다.
따라서 JUnit 기초를 학습하도록 한다.

JUnit 5은 5.5 버전을 기준으로 작성되었다.

JUnit 5 모듈 구성

  • JUnit 플랫폼 : 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API를 제공한다.
  • JUnit 주피터(Jupiter) : JUnit 5를 위한 테스트 API와 실행 엔진을 제공한다.
  • JUnit 빈티지(Vintage) : JUnit 3과 4로 작성된 테스트를 JUnit 5 플랫폼에서 실행하기 위한 모듈을 제공한다.

모듈 구조 이미지 - 출처

JUnit 5는 테스트를 위한 API로 주피터 API를 제공한다.
주피터 API를 사용해서 테스트를 작성하고 실행하려면 주피터 관련 모듈을 의존에 추가하면 된다.

메이븐 의존

<dependencies>
	<dependency>
    	<groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.5.0</version>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
	<plugins>
    	<plugin>
        	<artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.1</version>
        </plugin>
     </plugins>
 </build>

JUnit 5를 이용해서 테스트를 실행하려면 JUnit 5 플랫폼이 제공하는 플랫폼 런처를 사용해야 한다.
메이븐은 maven-surefire-plugin 2.22.0 버전부터 JUnit 5 플랫폼을 지원하므로 따로 플랫폼을 설정하지 않아도 된다.

그레이들 의존
JUnit 주피터 API를 테스트 구현으로 사용하고 JUnit 플랫폼을 이용해서 테스트를 실행하도록 설정하면 된다.

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.7'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation('org.junit.jupiter:junit-jupiter:5.5.0')
}

tasks.named('test') {
    useJUnitPlatform()
}

testImplementation을 사용해서 junit-jupiter 의존을 추가한다.
test 태스크는 JUnit 5 플랫폼을 사용하도록 설정한다.

그레이들 4.6 버전부터 JUnit 5 플랫폼을 지원한다. junit-jupiter 모듈은 JUnit 5.4 버전부터 제공한다. JUnit 5.3 버전이나 그 이하 버전을 사용한다면 필요한 모듈을 각각 설정해야 한다.

@Test 애노테이션과 테스트 메서드

JUnit 모듈을 설정했다면 JUnit을 이용해서 테스트 코드를 작성하고 실행할 수 있다.
테스트로 사용할 클래스를 만들고 @Test 애노테이션을 메서드에 붙이면 된다.

import org.junit.jupiter.api.Test;

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

public class sumTest {

    @Test
    void sum() {
        int result = 2 + 3;
        assertEquals(5, result);
    }
}

테스트 클래스의 이름을 작성하는 특별한 규칙은 없지만 보통 다른 클래스와 구분을 쉽게 하기 위해 'Test'를 접미사로 붙인다. (위 코드에서는 sumTest 라고 작성했다.)
테스트를 실행할 메서드에는 @Test 애노테이션을 붙인다.
이때 @Test 애노테이션을 붙인 메서드는 private이면 안 된다.

JUnit의 Assertions 클래스는 assertEquals() 메서드와 같이 값을 검증하기 위한 목적의 다양한 정적 메서드를 제공한다.
테스트를 실행하는 메서드는 JUnit이 제공하는 검증 메서드를 이용해서 결과를 확인한다.

JUnit 5 버전과 JUnit 4 버전은 패키지 구조나 일부 제약 조건 등이 다르다. 예를 들어 JUnit 4에서는 @Test 애노테이션이 org.junit 패키지에 포함되어 있고 assertEquals() 메서드는 org.junit.Assert 클래스에 포함되어 있다. 이 외에도 몇 가지 차이점이 있다.

주요 단언 메서드

Assertions 클래스가 제공하는 주요 단언 메서드

  • assertEquals(expected, actual) : 실제 값(actual)이 기대하는 값(expected)과 같은지 검사한다.
  • assertNotEquals(unexpected, actual) : 실제 값(actual)이 특정 값(unexpected)과 같지 않은지 검사한다.
  • assertSame(Object expected, Object actual) : 두 객체가 동일한 객체인지 검사한다.
  • assertNotSame(Object unexpected, Object actual) : 두 객체가 동일하지 않은 객체인지 검사한다.
  • assertTrue(boolean condition) : 값이 true인지 검사한다.
  • assertFalse(boolean condition) : 값이 false인지 검사한다.
  • assertNull(Object actual) : 값이 null인지 검사한다.
  • assertNotNull(Object actual) : 값이 null이 아닌지 검사한다.
  • fail() : 테스트를 실패 처리한다.

주요 타입별로 assertEquals() 메서드가 존재한다.
(int 타입을 위한 assertEquals() 메서드, Long 타입을 위한 assertEquals() 메서드, Object를 위한 assertEquals() 메서드.. 등)

fail() 메서드는 테스트에 실패했음을 알리고 싶을 때 사용한다.
예를 들어, ID와 암호로 전달받은 파라미터 값이 null이면 IllegalArgumentException이 발생하도록 인증 기능을 구현했다고 가정한다. 테스트 코드는 ID와 암호로 null을 전달했는데 익셉션이 발생하지 않으면 테스트에 실패했다고 볼 수 있다. 이럴 때 fail()을 사용할 수 있다.

try {
    AuthService authService = new AuthService();
    authService.authenticate(null, null);
    fail();
} catch (IllegalArgumentException e) {}

익셉션 발생 유무가 검증 대상이라면 fail() 메서드를 사용하는 것보다 아래 두 메서드를 사용하는 것이 더욱 명시적이다.

Assertions가 제공하는 익셉션 발생 유무 검사 메서드

  • assertThrows(Class<<T>> expectedType, Executable executable) : executable을 실행한 결과로 지정한 타입의 익셉션이 발생하는지 검사한다.
  • assertDoesNotThrow(Executable executable) : executable을 실행한 결과로 익셉션이 발생하지 않는지 검사한다.
assertThrows(IllegalArgumentException.class, () -> {
            AuthService authService = new AuthService();
            authService.authenticate(null, null);
        });

assertThrows() 메서드는 발생한 익셉션 객체를 리턴한다.
발생한 익셉션을 이용해서 추가로 검증이 필요하면 assertThrows() 메서드가 리턴한 익셉션 객체를 사용하면 된다.

IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
        () -> {
            AuthService authService = new AuthService();
            authService.authenticate(null, null);
        });
assertTrue(thrown.getMessage().contains("id"));

assert 메서드는 실패하면 다음 코드를 실행하지 않고 바로 익셉션을 발생한다.
아래 코드를 보면, 첫 번째 assertEquals() 메서드에서 검증에 실패하기 때문에 그 시점에 AssertionFailedError를 발생한다.
따라서 두 번째 assertEquals() 메서드는 실행되지 않는다.

assertEquals(3, 5/2);
assertEquals(4, 2 * 2);

위와 같은 상황을 해결하고 싶을 때,
경우에 따라 일단 모든 검증을 실행하고 그 중에 실패한 것이 있는지 확인할 수 있다.
assertAll() 메서드를 사용하면 된다.

assertAll(
        () -> assertEquals(3, 5/2),
        () -> assertEquals(4, 2 * 2),
        () -> assertEquals(6, 11 / 2)
);

assertAll() 메서드는 Executable 목록을 가변 인자로 전달받아 각 Executable을 실행한다.
실행 결과로 검증에 실패한 코드가 있으면 그 목록을 모아서 에러 메시지로 보여준다.

테스트 라이프사이클

@BeforeEach 애노테이션과 @AfterEach 애노테이션

JUnit은 각 테스트 메서드마다 다음 순서대로 코드를 실행한다.

  1. 테스트 메서드를 포함한 객체 생성
  2. (존재하면) @BeforeEach 애노테이션이 붙은 메서드 실행
  3. @Test 애노테이션이 붙은 메서드 실행
  4. (존재하면) @AfterEach 애노테이션이 붙은 메서드 실행

위 순서의 동작 방식을 이해하기 위해 코드를 작성해본다.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class LifecycleTest {

    public LifecycleTest() {
        System.out.println("new LifecycleTest");
    }

    @BeforeEach
    void setUp() {
        System.out.println("setUp");
    }

    @Test
    void a() {
        System.out.println("A");
    }

    @Test
    void b() {
        System.out.println("B");
    }

    @AfterEach
    void tearDown() {
        System.out.println("tearDown");
    }
}

결과는 아래와 같다.

결과를 보면, @Test 메서드를 실행할 때마다 객체를 새로 생성하고 테스트 메서드를 실행하기 전과 후에 @BeforeEach 애노테이션과 @AfterEach 애노테이션을 붙인 메서드를 실행한다는 것을 알 수 있다.

@BeforeEach 애노테이션은 테스트를 실행하는데 필요한 준비 작업을 할 때 사용한다.
@BeforeEach 애노테이션을 이용해서 테스트에서 임시 파일을 생성한다거나 테스트 메서드에서 사용할 객체를 생성한다.

반대로 @AfterEach 애노테이션은 테스트를 실행한 후에 정리할 것이 있을 때 사용한다.
테스트에서 사용한 임시 파일을 삭제해야 할 때 @AfterEache 애노테이션을 사용하면 된다.

@BeforeEach 애노테이션과 @AfterEach 애노테이션을 붙인 메서드는 @Test 애노테이션과 마찬가지로 private 이면 안된다.

@BeforeAll 애노테이션과 @AfterAll 애노테이션

한 클래스의 모든 테스트 메서드가 실행되기 전에 특정 작업을 수행해야 한다면 @BeforeAll 애노테이션을 사용한다.
@BeforeAll 애노테이션은 정적 메서드에 붙이는데 이 메서드는 클래스의 모든 테스트 메서드를 실행하기 전에 한 번 실행된다.

@AfterAll 애노테이션은 반대로 클래스의 모든 테스트 메서드를 실행한 뒤에 실행된다. 이 메서드 역시 정적 메서드에 적용한다.


테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기

public class BadTest {
    private FileOperator op = new FileOperator();
    private static File file; // 두 테스트가 데이터를 공유할 목적으로 필드 사용

    @Test
    void fileCreationTest() {
        File createdFile = op.createFile();
        assertTrue(createdFile.length() > 0);
        this.file = createdFile;
    }

    @Test
    void readFileTest() {
        long data = op.readData(file);
        assertTrue(data > 0);
    }
}

위 코드는 fileCreationTest() 메서드에서 생성한 File을 file 필드에 보관하고, 이 필드를 readFileTest() 메서드에서 사용한다.
테스트 메서드를 실행할 때마다 객체를 새로 생성하므로 file을 정적 필드로 정의했다.

이 테스트는 fileCreationTest() 메서드가 readFileTest() 메서드보다 먼저 실행된다는 것을 가정한다.

실제 원하는 순서대로 테스트 메서드가 실행될 수도 있다.
하지만 테스트 메서드가 특정 순서대로 실행된다는 가정하에 테스트 메서드를 작성하면 안 된다.

JUnit이 테스트 순서를 결정하지만 그 순서는 버전에 따라 달라질 수 있다.
순서가 달라지면 테스트도 실패한다.

✅ 각 테스트 메서드는 서로 독립적으로 동작해야 한다.
한 테스트 메서드의 결과에 따라 다른 테스트 메서드의 실행 결과가 달라지면 안 된다.

JUnit은 테스트 메서드의 실행 순서를 지정하는 방법을 제공하고 있다. 하지만 각 테스트 메서드는 독립적으로 동작해야 한다. 테스트 메서드 간에 의존이 생기면 이는 테스트 코드의 유지보수를 어렵게 만든다.

추가 애노테이션 : @DisplayName, @Disabled

자바는 메서드 이름에 공백이나 특수 문자를 사용할 수 없기 때문에 메서드 이름만으로 테스트 내용을 설명하기가 부족할 수 있다.
이럴 때는 @DisplayName 애노테이션을 사용해서 테스트에 표시 이름을 붙일 수 있다.

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("@DisplayName 테스트")
public class DisplayNameTest {

    @DisplayName("값 같은지 비교")
    @Test
    void assertEqualsMethod() {
        System.out.println("assertEqualsMethod");
    }

    @Disabled
    @Test
    void failMethod() {
        System.out.println("failMethod");
    }

    @DisplayName("익셉션 발생 여부 테스트")
    @Test
    void assertThrowsTest() {
        System.out.println("assertThrowsTest");
    }

    @Test
    void assertAllTest() {
        System.out.println("assertAllTest");
    }
}

특정 테스트를 실행하지 않고 싶을 때는 @Disabled 애노테이션을 사용한다. JUnit은 @Disabled 애노테이션이 붙은 클래스나 메서드는 테스트 실행 대상에서 제외한다.

아직 테스트 코드가 완성되지 않았거나 잠시 동안 테스트를 실행하지 말아야 할 때 이 애노테이션을 사용한다.

모든 테스트 실행하기

개발하는 과정에서는 특정 테스트 클래스나 메서드만 실행하지만, 코드를 원격 리포지토리에 푸시하거나 코드를 빌드해서 운영 환경에 배포하기 전에는 모든 테스트를 실행해서 깨지는 테스트가 없는지 확인한다.

모든 테스트를 실행하는 방법은 간단하다.

  • 메이븐 : mvn test (래퍼를 사용하는 경우 mvnw test)
  • 그레이들 : gradle test (래퍼를 사용하는 경우 gradlew test)

전체 테스트를 실행하면 몇 개의 테스트를 실행했고 그 중에 몇 개를 통과했는지 확인할 수 있다.

0개의 댓글