JUnit 사용법

Hyeokwoo Kwon·2021년 11월 30일
1
post-thumbnail

이전에 SpringBoot 프로젝트를 하나 진행했는데, 그때 사용했던 테스트 프레임워크가 JUnit이었다. 당시에는 내가 작성하는 테스트가 JUnit인지도 모르고 팀원이 작성한 다른 모듈의 테스트 코드를 참고하며 급하게 작성했었는데, 최근에서야 그게 JUnit이라는 걸 알게 되었다. spring-boot-test에 포함되어 있어서 몰랐나...? 어쨌든, 이번 글에서는 JUnit이 무엇인지 간단하게 정리하고, JUnit이 제공하는 주요 기능을 정리해 보려고 한다.


JUnit의 기능

JUnitJava에서 단위 테스트를 수행하기 위한 도구이다. 이 글은 현재 최신 버전인 JUnit5를 기준으로 작성했다. JUnit4 때에는 J2SE 5.0까지 지원했지만, JUnit5를 사용하기 위해서는 프로젝트의 jdk 버전이 최소 Java 8은 되어야 한다.

JUnit5에서는 JUnit4 때 기본으로 포함되어 있던 hamcrest 라이브러리가 빠졌기 때문에, assertThat 등 hamcrest 라이브러리에 포함된 기능을 사용하기 위해서는 hamcresthamcrest-core 라이브러리를 프로젝트로 가져와야 한다.

maven 프로젝트에서 JUnit을 사용하려면 pom.xml에 다음과 같이 의존성을 추가하면 된다.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>

gradle 프로젝트의 경우는 build.gradle에 다음과 같이 의존성을 추가하여 사용하면 된다.

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

테스트 진행

Intellij 환경에서 Pure Java 프로젝트를 생성해서 직접 테스트를 진행해 보자.

먼저 아래와 같은 클래스를 만들었다.

public class Circle {

    private double x;
    private double y;
    private double radius;

    public Circle(double x, double y, double radius) throws Exception {
        if (radius < 0) {
            throw new Exception("반지름이 0보다 작습니다.");
        }
        this.x = x;
        this.y = y;
        this.radius = radius;
    }

    public boolean reach(Circle other) {
        double distance = Math.sqrt(
                (other.x - this.x) * ((other.x - this.x)) +
                (other.y - this.y) * (other.y - this.y)
        );
        return distance <= this.radius + other.radius;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Circle circle = (Circle) o;
        return Double.compare(circle.x, x) == 0 &&
               Double.compare(circle.y, y) == 0 &&
               Double.compare(circle.radius, radius) == 0;
    }
}

두 원이 서로 접하면 true를, 그렇지 않으면 false를 리턴하는 함수 reach()를 구현했다. 이제 이 함수에 대한 테스트 코드를 작성해서, 함수가 예상한 대로 작동하는지 확인해 보겠다.

JUnit 기본 assert 메소드

아래 목록은 org.junit.jupiter.api.Assertions 클래스에 정의되어 있는 static 메소드들이다.

형태설명
void assertTrue(boolean condition)conditiontrue이면 테스트에 성공한다.
void assertEquals(Object expected, Object actual)expectedactual 의 값이 같으면 테스트에 성공한다.
void assertSame(Object expected, Object actual)expectedactual이 같은 레퍼런스이면 테스트에 성공한다.
void assertNull(Object actual)actualnull이면 테스트에 성공한다.
void assertAll(Executable... executables)인자로 들어온 모든 executable들이 예외를 발생시키지 않으면 테스트에 성공한다.
void assertThrows(Class<T> expectedType, Executable executable)인자로 들어온 executableexpectedType 타입의 예외를 발생시키는지 확인한다.
void assertTimeout(Duration timeout, Executable executable)executabletimeout 안에 수행을 완료하는지 확인한다.
void assertTimeoutPreemptively(Duration timeout, Executable executable)executabletimeout 안에 수행을 완료하는지 확인한다.

이 표만 봐서는 설명이 잘 와닿지 않는 메소드가 있을 것이다. 예시를 통해 하나씩 자세하게 살펴보자.

assertTrue

void assertTrue(boolean condition) 의 형태.
conditiontrue면 테스트에 성공하고, false면 테스트에 실패한다.
반대로 conditionfalse일 때 테스트에 성공하는 void assertFalse(boolean condition) 메소드도 있다.

assertTrue()assertFalse()를 테스트해 보자.

예시

import org.junit.jupiter.api.Test;

// assertTrue는 org.junit.jupiter.api 하위의 Assertions 클래스의 메소드이다.
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;

class CircleTest {

    @Test
    void assertTrueTest() {
        Circle circleA = new Circle(0, 0, 10);
        Circle circleB = new Circle(10, 0, 10);
        assertTrue(circleA.reach(circleB));
    }

    @Test
    void assertFalseTest() {
        Circle circleA = new Circle(0, 0, 10);
        Circle circleB = new Circle(30, 0, 10);
        assertFalse(circleA.reach(circleB));
    }

각각의 테스트는 아래 이미지와 같은 상황이다.


assertTrueTest()의 경우는 두 원이 서로 접하므로 reach()의 결과가 true가 될 것이고, 따라서 테스트에 성공할 것으로 예상할 수 있다. 또, assertFalseTest()의 경우는 두 원이 서로 접하지 않으므로 reach()의 결과가 false가 될 것이고, 따라서 이 경우 또한 테스트에 성공할 것으로 예상할 수 있다.

테스트에 성공했다.

assertEquals

void assertEquals(Object expected, Object actual) 의 형태.
equals() 메소드로 값을 비교한다. expectedactual의 값이 같으면 테스트에 성공, 다르면 테스트에 실패한다.
반대의 경우인 expectedactual의 값이 다를 때에 테스트에 성공하는 void assertNotEquals(Object expected, Object actual) 메소드도 있다.

assertEquals()assertNotEquals()를 테스트해 보자.

예시

import org.junit.jupiter.api.Test;

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

class CircleTest {

    @Test
    void assertEqualsTest() {
        Circle circleA = new Circle(0, 0, 10);
        Circle circleB = new Circle(0, 0, 10);
        assertEquals(circleA, circleB);
    }

    @Test
    void assertNotEqualsTest() {
        Circle circleA = new Circle(0, 0, 10);
        Circle circleB = new Circle(10, 0, 10);
        assertNotEquals(circleA, circleB);
    }
}

assertEqualsTest()를 보면, circleAcircleB의 값이 동일한 것을 확인할 수 있다. 따라서 테스트에 성공할 것으로 예상할 수 있다. 또, assertNotEqualsTest()에 있는 circleAcircleB는 값이 서로 다르므로 테스트에 성공할 것으로 예상할 수 있다.

테스트에 성공했다.

assertSame

void assertSame(Object expected, Object actual) 의 형태.
expectedactual이 같은 객체인지 확인하는 데 사용한다. 즉, 동일한 레퍼런스인지 확인한다. 동일한 레퍼런스라면 테스트에 성공, 아니라면 테스트에 실패한다.
동일한 레퍼런스가 아닐 때 테스트에 성공하는 void assertNotSame(Object unexpected, Object actual) 메소드도 있다.

예시

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertNotSame;

class CircleTest {

    @Test
    void assertSameTest() {
        Circle circleA = new Circle(0, 0, 10);
        Circle circleB = circleA;
        assertSame(circleA, circleB);
    }

    @Test
    void assertNotSameTest() {
        Circle circleA = new Circle(0, 0, 10);
        Circle circleB = new Circle(0, 0, 10);
        assertNotSame(circleA, circleB);
    }
}

assertSameTest()에서는 두 변수를 동일한 레퍼런스로 만들었고, assertNotSameTest()에서는 각 변수를 각자 다른 객체의 레퍼런스로 만들었다.

테스트에 성공했다.

assertNull

void assertNull(Object actual) 의 형태.
actualnull이면 테스트에 성공한다. actualnull이 아니어야 테스트에 성공하는 void assertNotNull(Object actual) 메소드도 있다. nonNull이 아닌 notNull임에 유의하자.

예시

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertNotSame;

class CircleTest {

    @Test
    void assertNullTest() {
        Circle circle = null;
        assertNull(circle);
    }

    @Test
    void assertNotNullTest() {
        Circle circle = new Circle(0, 0, 10);
        assertNotNull(circle);
    }
}


테스트에 성공했다.

assertAll

void assertAll(Executable... executables) 의 형태.
인자로 들어온 모든 executable들이 예외를 발생시키지 않는지 확인한다. 인자의 executables는 각 테스트를 말하고, 예외를 발생시킨다는 것은 테스트가 실패한다는 것이다. 다시 말해, 인자로 넣은 모든 테스트들의 성공/실패 결과를 알려준다. 주로 여러 개의 assertSomething() 메소드들을 한 번에 검증할 때 사용한다.

Executable은 JUnit에 정의되어 있는 함수형 인터페이스이다. Runnable을 예외를 던질 수 있도록 재정의한 클래스라고 생각하면 된다.

예시

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertAll;

class CircleTest {

    @Test
    void assertAllTest() {
        Circle circleA = new Circle(-10, 0, 10);
        Circle circleB = new Circle(10, 0, 10);
        assertAll(
                () -> assertNotNull(circleA),
                () -> assertNotNull(circleB),
                () -> assertNotEquals(circleA, circleB),
                () -> assertTrue(circleA.reach(circleB))
        );
    }
}

assertAll()을 사용해 circleAnull이 아닌지, circleBnull이 아닌지, circleAcircleB의 값이 서로 다른지, circleAcircleB가 서로 접하는지를 한 번에 검증하는 테스트 케이스를 작성했다.

테스트에 성공했다. 그럼, 여기서 circleAnull로 만들고 테스트하면 어떻게 될까?

org.opentest4j.AssertionFailedError: expected: not <null>
at tests.CircleTest.lambda$assertAllTest$1
at java.base/java.util.stream.ReferencePipeline.collect
at tests.CircleTest.assertAllTest
at java.base/java.util.ArrayList.forEach
at java.base/java.util.ArrayList.forEach

java.lang.NullPointerException
at tests.CircleTest.lambda$assertAllTest$3
at java.base/java.util.stream.ReferencePipeline.collect
at tests.CircleTest.assertAllTest
at java.base/java.util.ArrayList.forEach
at java.base/java.util.ArrayList.forEach

expected: not <null> 이라는 문구로 assertNotNull() 부분에서 문제가 생겼다는 것 뿐만이 아니라, 그 밑의 NullPointerException을 통해 assertTrue() 부분에서도 문제가 발생했다는 것을 알 수 있다. assertAll()을 사용하지 않고 각 assert...()을 사용했다면 assertNotNull() 부분에서 발생한 문제에 대해서만 인지할 수 있었을 것이다.

assertThrows

void assertThrows(Class<T> expectedType, Executable executable) 의 형태.
인자로 들어온 executableexpectedType 타입의 예외를 발생시키는지 확인한다. 예외가 발생하면 테스트에 성공하고, 예외가 발생하지 않으면 테스트에 실패한다.
반대의 역할을 하는 void assertDoesNotThrow(Executable executable) 메소드도 있다. 인자를 보면 assertThrows()와 달리 expectedType을 받지 않는데, 예외가 발생하지 않는다는 것을 확인하는 데 쓰는 메소드이므로 예외의 타입을 인자로 받아올 필요가 없어서 그런 것이다.

예시

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;

class CircleTest {

    @Test
    void assertThrowsTest() {
        assertThrows(Exception.class, () -> new Circle(0, 0, -10));
    }

    @Test
    void assertDoesNotThrowTest() {
        assertDoesNotThrow(() -> new Circle(0, 0, 10));
    }
}

앞에서 정의했던 클래스를 보면, 객체를 생성할 때 반지름이 0보다 작으면 예외를 발생시키도록 구현한 것을 볼 수 있다. assertThrowsTest()에서는 일부러 예외를 발생시키기 위해 반지름을 -10으로 주었다. 또, assertDoesNotThrowTest() 에서는 예외가 발생하지 않도록 생성자를 불렀다.

테스트에 성공했다.

assertTimeout

void assertTimeout(Duration timeout, Executable executable)의 형태.
executabletimeout 안에 수행을 완료하는지 확인한다. timeout 안에 executable이 실행을 마치면 테스트에 성공하고, timeout이 지났는데도 executable의 실행이 완료되지 않았으면 테스트에 실패한다.

예시

import org.junit.jupiter.api.Test;

import java.time.Duration;

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

class CircleTest {

    @Test
    void assertTimeoutSuccessTest() {
        assertTimeout(Duration.ofMillis(1000), () -> {
            System.out.println("executable 실행");
        });
    }

    @Test
    void assertTimeoutFailureTest() {
        assertTimeout(Duration.ofMillis(1000), () -> {
            Thread.sleep(1500);
            System.out.println("executable 실행");
        });
    }
}

테스트에 실패했을 때 어떻게 되는지 보기 위해서, assertTimeOutFailure()는 일부러 테스트에 실패하도록 작성했다.

1000ms 안에 작업을 끝내야 테스트에 성공하는데, 501ms를 초과해서 테스트에 실패했다고 알려준다. 여기서 알 수 있는 것이, assertTimeout()은 인자로 주어진 timeout이 지났는데도 실행이 끝나지 않았으면 모듈의 실행이 끝날 때까지 기다린 뒤에 실행 시간이 얼마나 걸렸는지 알려 준다는 것이다. 전체 모듈에 대한 테스트를 수행하는 경우에 assertTimeout() 을 사용하면 실행 시간이 긴 모듈 하나 때문에 전체 테스트의 시간이 길어질 수 있다. 물론 모듈의 수행 시간을 알아야 하는 경우에는 이대로 사용해도 좋다. 하지만, timeout 안에 모듈이 수행을 마치는지, 혹은 아닌지만 알면 되는 경우에는 굳이 기다릴 필요가 없다. 따라서 timeout 이내에 실행을 마쳤는지에 대해서만 알면 되는 경우에는 assertTimeoutPreemptively() 를 사용해서 테스트를 작성하면 된다.

assertTimeoutPreemptively

assertTimeout과 비슷하지만, executabletimeout 안에 수행을 마치지 못한 경우에 즉시 테스트를 종료한다.

import org.junit.jupiter.api.Test;

import java.time.Duration;

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

class CircleTest {

    @Test
    void assertTimeoutPreemptivelyTest() {
        assertTimeoutPreemptively(Duration.ofMillis(1000), () -> {
            Thread.sleep(3600000);
            System.out.println("정말 긴 작업 실행");
        });
    }
}

1시간이 걸리는 정말 긴 작업이다. assertTimeout() 이었다면 이 모듈의 실행이 끝날 때까지 1시간을 기다려야 했을 것이다.

설정한 timeout이 지나자 테스트를 바로 종료한 것을 확인할 수 있다.

Annotations

아래 내용은 공식 사이트를 참고하여 작성했다. 모든 것을 설명하긴 분량이 많아서 이 글에선 몇 개만 간단하게 살펴 보고, 예시를 포함한 자세한 내용은 추후에 다른 글에서 다룰 생각이다.

@Test

테스트 메소드임을 나타낸다.

@TestMethodOrder

여러 테스트 메소드를 동시에 실행하는 경우에 사용하는, 실행 순서를 지정하는 어노테이션이다.

@DisplayName

테스트 클래스나 메소드에 콘솔에서 출력될 이름을 부여한다.

@BeforeEach

각 테스트 메소드를 실행하기 전에 실행되는 메소드를 지정할 때 사용하는 어노테이션.

@BeforeAll

모든 테스트 메소드를 실행하기 전에 단 한 번 실행되어야 하는 메소드를 지정할 때 사용하는 어노테이션.

@AfterEach

각 테스트 메소드를 실행한 뒤 실행되는 메소드를 지정할 때 사용하는 어노테이션.

@AfterAll

모든 테스트 메소드가 실행된 뒤 단 한 번 실행되어야 하는 메소드를 지정할 때 사용하는 어노테이션.

0개의 댓글