이전에 SpringBoot 프로젝트를 하나 진행했는데, 그때 사용했던 테스트 프레임워크가 JUnit이었다. 당시에는 내가 작성하는 테스트가 JUnit인지도 모르고 팀원이 작성한 다른 모듈의 테스트 코드를 참고하며 급하게 작성했었는데, 최근에서야 그게 JUnit이라는 걸 알게 되었다. spring-boot-test에 포함되어 있어서 몰랐나...? 어쨌든, 이번 글에서는 JUnit이 무엇인지 간단하게 정리하고, JUnit이 제공하는 주요 기능을 정리해 보려고 한다.
JUnit은 Java에서 단위 테스트를 수행하기 위한 도구이다. 이 글은 현재 최신 버전인 JUnit5를 기준으로 작성했다. JUnit4 때에는 J2SE 5.0까지 지원했지만, JUnit5를 사용하기 위해서는 프로젝트의 jdk 버전이 최소 Java 8은 되어야 한다.
JUnit5에서는 JUnit4 때 기본으로 포함되어 있던 hamcrest 라이브러리가 빠졌기 때문에, assertThat
등 hamcrest 라이브러리에 포함된 기능을 사용하기 위해서는 hamcrest와 hamcrest-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()를 구현했다. 이제 이 함수에 대한 테스트 코드를 작성해서, 함수가 예상한 대로 작동하는지 확인해 보겠다.
아래 목록은 org.junit.jupiter.api.Assertions 클래스에 정의되어 있는 static 메소드들이다.
형태 | 설명 |
---|---|
void assertTrue(boolean condition) | condition 이 true 이면 테스트에 성공한다. |
void assertEquals(Object expected, Object actual) | expected 와 actual 의 값이 같으면 테스트에 성공한다. |
void assertSame(Object expected, Object actual) | expected 와 actual 이 같은 레퍼런스이면 테스트에 성공한다. |
void assertNull(Object actual) | actual 이 null 이면 테스트에 성공한다. |
void assertAll(Executable... executables) | 인자로 들어온 모든 executable 들이 예외를 발생시키지 않으면 테스트에 성공한다. |
void assertThrows(Class<T> expectedType, Executable executable) | 인자로 들어온 executable 이 expectedType 타입의 예외를 발생시키는지 확인한다. |
void assertTimeout(Duration timeout, Executable executable) | executable 이 timeout 안에 수행을 완료하는지 확인한다. |
void assertTimeoutPreemptively(Duration timeout, Executable executable) | executable 이 timeout 안에 수행을 완료하는지 확인한다. |
이 표만 봐서는 설명이 잘 와닿지 않는 메소드가 있을 것이다. 예시를 통해 하나씩 자세하게 살펴보자.
void assertTrue(boolean condition)
의 형태.
condition
이 true
면 테스트에 성공하고, false
면 테스트에 실패한다.
반대로 condition
이 false
일 때 테스트에 성공하는 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
가 될 것이고, 따라서 이 경우 또한 테스트에 성공할 것으로 예상할 수 있다.
테스트에 성공했다.
void assertEquals(Object expected, Object actual)
의 형태.
equals()
메소드로 값을 비교한다. expected
와 actual
의 값이 같으면 테스트에 성공, 다르면 테스트에 실패한다.
반대의 경우인 expected
와 actual
의 값이 다를 때에 테스트에 성공하는 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()
를 보면, circleA
와 circleB
의 값이 동일한 것을 확인할 수 있다. 따라서 테스트에 성공할 것으로 예상할 수 있다. 또, assertNotEqualsTest()
에 있는 circleA
와 circleB
는 값이 서로 다르므로 테스트에 성공할 것으로 예상할 수 있다.
테스트에 성공했다.
void assertSame(Object expected, Object actual)
의 형태.
expected
와 actual
이 같은 객체인지 확인하는 데 사용한다. 즉, 동일한 레퍼런스인지 확인한다. 동일한 레퍼런스라면 테스트에 성공, 아니라면 테스트에 실패한다.
동일한 레퍼런스가 아닐 때 테스트에 성공하는 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()
에서는 각 변수를 각자 다른 객체의 레퍼런스로 만들었다.
테스트에 성공했다.
void assertNull(Object actual)
의 형태.
actual
이 null
이면 테스트에 성공한다. actual
이 null
이 아니어야 테스트에 성공하는 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);
}
}
테스트에 성공했다.
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()
을 사용해 circleA
가 null
이 아닌지, circleB
가 null
이 아닌지, circleA
와 circleB
의 값이 서로 다른지, circleA
와 circleB
가 서로 접하는지를 한 번에 검증하는 테스트 케이스를 작성했다.
테스트에 성공했다. 그럼, 여기서 circleA
을 null
로 만들고 테스트하면 어떻게 될까?
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()
부분에서 발생한 문제에 대해서만 인지할 수 있었을 것이다.
void assertThrows(Class<T> expectedType, Executable executable)
의 형태.
인자로 들어온 executable
이 expectedType
타입의 예외를 발생시키는지 확인한다. 예외가 발생하면 테스트에 성공하고, 예외가 발생하지 않으면 테스트에 실패한다.
반대의 역할을 하는 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()
에서는 예외가 발생하지 않도록 생성자를 불렀다.
테스트에 성공했다.
void assertTimeout(Duration timeout, Executable executable)
의 형태.
executable
이 timeout
안에 수행을 완료하는지 확인한다. 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()
를 사용해서 테스트를 작성하면 된다.
assertTimeout
과 비슷하지만, executable
이 timeout
안에 수행을 마치지 못한 경우에 즉시 테스트를 종료한다.
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
이 지나자 테스트를 바로 종료한 것을 확인할 수 있다.
아래 내용은 공식 사이트를 참고하여 작성했다. 모든 것을 설명하긴 분량이 많아서 이 글에선 몇 개만 간단하게 살펴 보고, 예시를 포함한 자세한 내용은 추후에 다른 글에서 다룰 생각이다.
테스트 메소드임을 나타낸다.
여러 테스트 메소드를 동시에 실행하는 경우에 사용하는, 실행 순서를 지정하는 어노테이션이다.
테스트 클래스나 메소드에 콘솔에서 출력될 이름을 부여한다.
각 테스트 메소드를 실행하기 전에 실행되는 메소드를 지정할 때 사용하는 어노테이션.
모든 테스트 메소드를 실행하기 전에 단 한 번 실행되어야 하는 메소드를 지정할 때 사용하는 어노테이션.
각 테스트 메소드를 실행한 뒤 실행되는 메소드를 지정할 때 사용하는 어노테이션.
모든 테스트 메소드가 실행된 뒤 단 한 번 실행되어야 하는 메소드를 지정할 때 사용하는 어노테이션.