단위 테스트
단위 테스트는 애플리케이션의 각 기능이 독립적으로 올바르게 동작하는지를 검증하는 프로세스이다.
JUnit 5
JUnit 5는 단위 테스트 프레임워크로, XUnit 계열 중 하나이다.
- JUnit Platform: JVM에서 동작하는 테스트 프레임워크로, TestEngine 인터페이스를 정의하고 테스트를 발견, 실행 및 결과를 보고하는 역할을 한다.
- JUnit Jupiter: JUnit 5를 위한 테스트 API를 사용하는 TestEngine 구현체로, Jupiter API를 통해 작성한 테스트 코드를 실행한다.
- JUnit Vintage: JUnit 3 및 4 버전으로 작성한 테스트 코드를 실행하는 TestEngine 구현체이다.
JUnit 5는 Java 8 이상과 Spring Boot 2.2 이상에서 사용 가능하며, 의존성을 추가하여 사용할 수 있다.
JUnit 5의 주요 어노테이션
1. 테스트 메서드와 생명 주기
- @Test: 이 어노테이션은 메서드가 테스트 메서드임을 나타낸다. JUnit은 이 어노테이션이 붙은 메서드를 자동으로 테스트로 인식하여 실행한다.
- @BeforeEach: 각 테스트 메서드 실행 전에 특정 작업을 수행해야 할 때 사용된다. 주로 목업 데이터를 설정하는 데 유용하다.
private Calculator calculator;
@BeforeEach
void setUp() {
// 각 테스트 메서드 실행 전에 Calculator 객체를 초기화
calculator = new Calculator();
}
@Test
void testAddition() {
assertEquals(5, calculator.add(2, 3));
}
@Test
void testSubtraction() {
assertEquals(1, calculator.subtract(3, 2));
}
- @AfterEach: 각 테스트 메서드 실행 후 특정 작업을 수행할 때 사용된다. 이 메서드는 모든 테스트 메서드 실행 후에 실행된다.
- @BeforeAll: 모든 테스트 메서드 실행 전 한 번만 특정 작업을 수행한다.
- @AfterAll: 모든 테스트 메서드 실행 후 한 번만 특정 작업을 수행하는 static 메서드이다. BeforeAll/AfterAll 메서드는 static으로 정의되어야 한다.
- @Nested: 테스트 클래스 안에 내부 클래스를 정의하여 테스트를 계층화할 수 있다. 내부 클래스는 부모 클래스의 멤버 필드에 접근할 수 있는 장점이 있다.
@Nested
class 우승자_출력 {
@Test
void 여러_우승자_출력_테스트() {
// Given
List<String> winners = List.of("pobi", "woni");
// When
Application.printWinner(winners);
// Then
assertThat(output()).contains("pobi, woni");
}
}

2. 유틸리티 어노테이션
- @DisplayName: 메서드 이름에 사용할 수 없는 공백, 특수 문자, 이모지를 활용하여 사용자 친화적으로 표시할 수 있다. 이를 통해 테스트의 의도를 명확하게 전달할 수 있다.
@DisplayName("덧셈 테스트")
@Test
void testAddition() {
assertEquals(2, 1 + 1);
}
3. 실행 조건
- @Disabled: 테스트를 실행하고 싶지 않은 클래스나 메서드에 붙인다. 이 어노테이션이 붙은 테스트는 JUnit에서 실행되지 않는다.
@Disabled
@Test
void skippedTest() {
// 이 테스트는 실행되지 않는다.
}
4. 테스트 종류
- @ParameterizedTest: 여러 입력 값에 대해 반복적으로 테스트를 수행할 수 있게 해주는 어노테이션이다. 여러 파라미터 어노테이션과 함께 사용된다.
[[codingPractices] Parameterized Tests 사용법](https://velog.io/@grayson1999/codingPractices-Parameterized-Tests-%EC%82%AC%EC%9A%A9%EB%B2%95)
@ParameterizedTest
@CsvSource({"1, 2, 3", "2, 3, 5", "3, 5, 8"})
void testAddition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
- @RepeatedTest: 지정된 횟수만큼 테스트를 실행할 수 있도록 해주는 어노테이션이다.
@RepeatedTest(5)
void repeatedTest() {
// 이 테스트는 5번 실행된다.
}
AssertJ
Assertions는 테스트 케이스의 수행 결과를 판별하는 메서드로, 모든 JUnit Jupiter assertions는 static 메서드로 제공된다. Assertions를 통해 실제 결과와 예상 결과를 비교할 수 있다.
AssertJ 사용법
AssertJ의 기본 문법 구조
assertThat(검증하려는 대상).검증메서드(원하는 결과);
이 구조를 통해 간단하고 직관적인 방식으로 테스트를 작성할 수 있다. 검증하려는 대상을 지정한 후, 그에 대한 검증 메서드를 호출하여 원하는 결과와 비교할 수 있다.
주요 검증 메서드
아래는 분류별로 정렬한 표입니다:
| 분류 | 검증 메서드 | 설명 |
|---|---|---|
| boolean | isTrue() | 조건이 true인지 검증한다. |
| boolean | isFalse() | 조건이 false인지 검증한다. |
| object | isEqualTo(expected) | 기대하는 값과 동일한지 비교한다. |
| object | isNotEqualTo(unexpected) | 예상하지 않는 값과 다른지 확인한다. |
| object | isSameAs(expected) | 두 객체가 동일한 인스턴스인지 비교한다. |
| object | isNotSameAs(unexpected) | 두 객체가 서로 다른 인스턴스인지 확인한다. |
| object | isNull() | 주어진 값이 null인지 확인한다. |
| object | isNotNull() | 주어진 값이 null이 아님을 검증한다. |
| iterable/array | contains(expected) | 주어진 값이 컬렉션이나 문자열에 포함되는지 확인한다. |
| iterable/array | doesNotContain(unexpected) | 주어진 값이 컬렉션이나 문자열에 포함되지 않는지 확인한다. |
| iterable/array | hasSize(size) | 컬렉션이나 배열의 크기가 주어진 값과 동일한지 검증한다. |
| iterable/array | isEmpty() | 컬렉션이나 문자열이 비어 있는지 확인한다. |
| iterable/array | containsExactly(elements) | 컬렉션이나 배열이 정확히 주어진 요소들만 포함하는지 확인한다. |
| iterable/array | doesNotContainAnyElementsOf(expected) | 주어진 컬렉션에 예상하지 않는 요소가 포함되어 있지 않은지 확인한다. |
| string | startsWith(prefix) | 문자열이 주어진 접두사로 시작하는지 확인한다. |
| string | endsWith(suffix) | 문자열이 주어진 접미사로 끝나는지 확인한다. |
| throwable | hasMessage(message) | 예외 메시지가 특정 문자열과 일치하는지 확인한다. |
| throwable | hasNoCause() | 예외가 원인이 없는지 검증한다. |
이 표는 분류별로 정렬하여 각 검증 메서드와 그 설명을 쉽게 확인할 수 있도록 구성되었습니다.
예외 검증
Java 8에서 assertThatThrownBy(ThrowingCallable)를 사용하여 Throwable을 캡처한 후 assert한다.
import static org.assertj.core.api.Assertions.*;
assertThatThrownBy(() -> {
// 예외 발생 코드
}).isInstanceOf(IndexOutOfBoundsException.class)
.hasMessageContaining("Index: 2, Size: 2");
assertThatExceptionOfType(ExceptionType).isThrownBy() 구문을 사용해 작성할 수 있다.
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
assertThatExceptionOfType(IndexOutOfBoundsException.class)
.isThrownBy(() -> {
// 예외 발생 코드
}).withMessageMatching("Index: \\d+, Size: \\d+");
- 자주 발생하는 Exception의 경우 Exception별 메서드를 제공하고 있다.
assertThatIllegalArgumentException()assertThatIllegalStateException()assertThatIOException()assertThatNullPointerException()
BDD 애호가를 위한 when, then 단계 분리 방법
@Test
public void testException() {
// given some preconditions
// when
Throwable thrown = catchThrowable(() -> { throw new Exception("boom!"); });
// then
assertThat(thrown).isInstanceOf(Exception.class)
.hasMessageContaining("boom");
}
JUnit과 AssertJ의 조합
JUnit은 테스트의 실행 및 구조에 중점을 두고 있으며, 테스트 메서드의 설정, 실행 및 생명 주기를 관리하는 기능을 제공한다. 하지만 표현력은 상대적으로 부족하다.
반면, AssertJ는 자연어에 가까운 방식으로 작성할 수 있도록 하여 테스트 코드의 가독성을 높이고, 결과를 직관적으로 이해할 수 있도록 돕는다.
AssertJ의 메서드는 일반적인 조건문 형태로 작성되기 때문에 누구나 쉽게 읽고 이해할 수 있다.