외부활동/우아한테크코스 [프리코스]

테스트주도 개발 시작하기 - 챕터 5. JUnit

softmoca__ 2025. 11. 5. 20:56
목차

https://product.kyobobook.co.kr/detail/S000001248962

 

테스트 주도 개발 시작하기 | 최범균 - 교보문고

테스트 주도 개발 시작하기 | 작동하는 깔끔한 코드를 만드는 데 필요한 습관 - JUnit 5를 이용한 테스트 주도 개발 안내 - 테스트 작성과 설계를 위한 대역 - 테스트 가능한 설계 방법 안내 - 유지

product.kyobobook.co.kr

 

5-1. JUnit 5 모듈 구성

5-1-1. 세 가지 큰 축

JUnit 5는 사실 “하나의 라이브러리”가 아니라, 플랫폼 + 여러 엔진 모듈이 모여 있는 구조.

  • JUnit 플랫폼 (Platform)
    • 여러 테스트 프레임워크를 공통으로 실행할 수 있는 런처테스트 엔진용 API 제공
    • Maven, Gradle 같은 빌드 도구나 IDE가 이 플랫폼을 통해 테스트를 실행
  • JUnit 주피터 (Jupiter)
    • 우리가 흔히 말하는 “JUnit 5 스타일 테스트”를 위한 API + 엔진
    • @Test, @BeforeEach, Assertions.assertEquals() 같은 것들이 전부 Jupiter 쪽
  • JUnit 빈티지 (Vintage)
    • 오래된 JUnit 3, 4 스타일 테스트를 JUnit 5 플랫폼에서 돌릴 수 있게 해주는 엔진

5-1-2. junit-jupiter 의존성

현대 Java 프로젝트에서 우리가 보통 추가하는 건 junit-jupiter

이 안에는 다시 세 가지가 포함

  • junit-jupiter-api – @Test, Assertions 등 테스트 코드에서 쓰는 API
  • junit-jupiter-params – 파라미터라이즈드 테스트(@ParameterizedTest 등)
  • junit-jupiter-engine – 실제로 JUnit 5 테스트를 실행해주는 엔진
  • testImplementation으로 테스트 전용 의존성 추가
  • useJUnitPlatform()으로 JUnit 5 플랫폼 사용 선언

Maven도 비슷하게 junit-jupiter 의존성을 test scope로 넣고, surefire 플러그인이 2.22.0+ 버전이면 별도 설정 없이 JUnit 5를 지원

5-2. @Test 애노테이션과 테스트 메서드

5-2-1. JUnit 5 테스트의 기본 규칙

  • 테스트 클래스 이름은 보통 SomethingTest 처럼 Test 접미사를 많이 사용 (강제 규칙은 아니지만 관례).
  • @Test가 붙은 메서드가 실제 테스트 메서드가 됨.
  • Assertions 클래스(혹은 static import)를 사용해 검증.

5-2-2. 테스트 메서드의 제약

  • @Test 메서드는:
    • private 이면 안 됨 (JUnit이 리플렉션으로 호출해야 해서)
    • 보통 void 리턴 타입 (값을 리턴하기보다 assert로 검증)
    • 파라미터가 없는 게 기본 (파라미터가 있는 경우는 @ParameterizedTest 등 다른 애노테이션 사용)
    • static으로 만드는 것도 일반적으로 피함 (JUnit 5는 기본적으로 테스트마다 인스턴스를 새로 만들기 때문)

5-3. Assertions 클래스의 주요 단언 메서드

5-3-1. 값 비교 계열

  • assertEquals(expected, actual)
    • 실제 값이 기대값과 같은지
  • assertNotEquals(unexpected, actual)
    • 실제 값이 특정 값과 다른지
  • assertSame(expected, actual)
    • 같은 객체 인스턴스인지 (==)
  • assertNotSame(unexpected, actual)
    • 서로 다른 인스턴스인지
  • assertTrue(condition)
  • assertFalse(condition)
  • assertNull(actual)
  • assertNotNull(actual)
  • fail()
    • 강제로 테스트를 실패 처리할 때 사용 (예: 특정 분기까지 코드가 도달하면 안 되는 경우)
  • 값 비교(기본 타입, equals)
  • 참/거짓 조건 확인
  • null 관련 체크

5-4. 예외 검증 – assertThrows / assertDoesNotThrow

5-4-1. 예외가 발생해야 하는 상황

TDD 하다 보면 “이 상황에서는 반드시 예외가 발생해야 한다” 같은 요구사항이 많이 나온다

Assertions.assertThrows를 어떻게 쓰는지 자세히 설명

@Test
void 인증_정보가_null이면_예외() {
    assertThrows(IllegalArgumentException.class,
            () -> {
                AuthService authService = new AuthService();
                authService.authenticate(null, null);
            });
}
  • 첫 번째 인자: 기대하는 예외 타입
  • 두 번째 인자: 예외가 발생해야 할 실행 코드 블록 (람다)

여기서 두 번째 파라미터의 타입이 바로 Executable 인터페이스

  • Executable은 void execute() throws Throwable; 하나만 가진 함수형 인터페이스라,람다로 바로 넘길 수 있음.

5-4-2. 예외가 발생하면 안 되는 상황

반대로, 특정 코드 블록에서 예외가 발생하지 않아야 할 때는 assertDoesNotThrow를 사용

@Test
void 정상_인증_정보는_예외가_발생하지_않음() {
    assertDoesNotThrow(() -> {
        // 정상 인증 로직 호출
    });
}

이렇게 예외를 테스트로 명시해 놓으면, 나중에 구현이 바뀌어도 예외 규칙이 깨지지 않았는지 자동으로 검증할 수 있다.

5-5. assertAll – 여러 검증을 한 번에

5-5-1. 일반 assert의 한계

기본 assert 메서드는 하나라도 실패하면 바로 예외를 던지고 테스트를 중단

그래서 다음과 같은 코드가 있으면

assertEquals(10000, account.getBalance());
assertEquals("ACTIVE", account.getStatus());

첫 번째가 실패하면 두 번째는 아예 실행되지 않음 → 실패 정보가 하나만 보임.

5-5-2. assertAll로 묶기

assertAll은 여러 검증을 하나의 그룹으로 묶어서 실행하고,실패한 항목들을 모아서 보여붐

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

@Test
void 계좌_상태_검증() {
    assertAll(
            () -> assertEquals(10000, account.getBalance()),
            () -> assertEquals("ACTIVE", account.getStatus()),
            () -> assertTrue(account.isOwner("영철"))
    );
}

  • 각 검증을 람다로 감싸서 assertAll에 넘김
  • 세 검증을 모두 실행해 보고, 실패한 것들만 모아서 에러 메시지에 나열
  • 한 테스트에서 여러 값의 일관성 등을 한 번에 확인할 때 유용
  • 특히 객체의 여러 필드 값들을 동시에 비교할 때 좋음

5-6. 테스트 라이프사이클

5-6-1. @BeforeEach / @AfterEach

  • @BeforeEach
    • 각 테스트 메서드 실행 직전에 실행되는 메서드
    • 공통 준비 작업: 테스트용 객체 생성, 임시 파일 준비 등
  • @AfterEach
    • 각 테스트 메서드 실행 직후에 실행되는 메서드
    • 정리 작업: 임시 파일 삭제, DB 초기화 등

라이프사이클 순서는

  1. 테스트 클래스 인스턴스 생성
  2. @BeforeEach 메서드 실행
  3. @Test 메서드 실행
  4. @AfterEach 메서드 실행 
  • 각 테스트 메서드마다 이 사이클이 한 번씩 동작 (JUnit 5의 기본 전략: 테스트마다 객체를 새로 만듦)
  • @BeforeEach, @AfterEach 메서드도 private이면 안 됨 (JUnit이 리플렉션으로 호출해야 해서)

5-6-2. @BeforeAll / @AfterAll

@BeforeAll / @AfterAll은 테스트 전체에서 딱 한 번만 실행되는 준비/정리 코드

  • @BeforeAll
    • 이 테스트 클래스 내의 모든 테스트 실행 전에 한 번 실행
    • 예: 데이터베이스 연결 풀 초기화, 외부 서버 부팅, 무거운 자원 준비
  • @AfterAll
    • 모든 테스트 실행 후에 한 번 실행
    • 예: 연결 종료, 서버 종료, 임시 폴더 통째 삭제 등

기본 규칙

  • @BeforeAll, @AfterAll 메서드는 static이어야 함 (테스트 인스턴스 생성 전에 호출해야 하기 때문)

5-7. 테스트 메서드 간 실행 순서 의존 & 필드 공유 금지

테스트 메서드는 서로 독립적이어야 한다.

5-7-1. 실행 순서를 가정하면 안 된다

JUnit 5는 테스트 메서드 실행 순서를 별도로 지정하지 않으면 보장 X

그래서 다음과 같은 의존은 금물

  • “test1()이 먼저 돌아가야 test2()가 제대로 돌 수 있다”
  • “항상 A → B → C 순서로 실행될 것이다”라고 가정하고 코드 작성

이렇게 쓰면 환경/IDE/구성에 따라 순서가 달라지거나, 이후 테스트 추가/수정 시 순서가 깨져서 알 수 없는 실패가 나옴.

5-7-2. 필드를 공유해서는 안 된다

JUnit 5 기본 전략은 “테스트 메서드마다 테스트 클래스의 인스턴스를 새로 생성

그래도 아래처럼 쓰면 위험

class CounterTest {

    private int counter = 0;

    @Test
    void test1() {
        counter++;
        assertEquals(1, counter);
    }

    @Test
    void test2() {
        counter++;
        assertEquals(1, counter); // 순서/인스턴스 생성 전략에 따라 깨질 수 있음
    }
}
  • 인스턴스가 매 테스트마다 새로 만들어진다는 사실을 모르고, 필드 값을 공유한다고 착각하면 잘못된 테스트가 됨.
  • 반대로, PER_CLASS 같은 전략을 쓰면서 필드를 공유하면 테스트 메서드 순서·실행 여부에 따라 결과가 달라지는 비결정적 테스트가 생길 수 있음.
  • 각 테스트 메서드는 독립적으로 실행해도 항상 같은 결과가 나와야 한다.
  • 다른 테스트의 결과/부작용에 기대지 말 것.

5-8. 추가 애노테이션 – @DisplayName / @Disabled

5-8-1. @DisplayName – 테스트 이름 꾸미기

@DisplayName은 테스트를 실행할 때 출력되는 이름을 마음대로 바꿀 수 있다.

  • IDE 실행 결과나 리포트에서 한글/자연어로 테스트 목적이 보임
  • 메서드 이름은 Java 규칙(소문자 시작, 공백 X)에 맞추고, @DisplayName으로 사람이 읽기 좋은 문장을 쓰는 패턴이 많이 쓰임.

5-8-2. @Disabled – 일시적으로 테스트 비활성화

@Disabled를 테스트 메서드나 클래스에 붙이면, 해당 테스트는 실행 대상에서 제외

  • 편해서 막 쓰면 “영원히 잊혀진 테스트”가 쌓일 수 있음.
  • 일시적인 사용(예: 외부 시스템 문제, 아직 구현 안 된 기능에 대한 placeholder) 정도로만 쓰라고 암시적으로 경고

5-9. 모든 테스트 실행하기 – Maven / Gradle / IDE

5-9-1. 빌드 도구에서 전체 테스트

  • Maven
    • mvn test – 테스트만 실행
    • mvn package – 빌드 과정에서 자동으로 테스트도 함께 실행
  • Gradle
    • gradle test 혹은 ./gradlew test
    • gradle build 시에도 test task가 포함되어 실행

테스트는 CI배포 파이프라인에 꼭 들어가야 할 단계고, 이 명령들이 그 기본이 된다.

5-9-2. IDE(IntelliJ)에서 전체 테스트

  • src/test/java 폴더에서 우클릭 → Run 'All Tests'
  • 혹은 상단에 있는 test configuration을 “All in project/module”로 설정하고 실행

실행 결과:

  • 몇 개 테스트를 실행했는지
  • 그중 몇 개가 통과/실패/스킵됐는지
  • 실패한 테스트의 스택 트레이스

핵심정리

  • JUnit 5 모듈 구성
    • Platform / Jupiter / Vintage
    • 보통은 junit-jupiter 의존성 추가 + useJUnitPlatform() 설정
  • @Test와 테스트 메서드 규칙
    • void, non-private
    • 보통 SomethingTest 관례 사용
  • Assertions 주요 메서드
    • assertEquals, assertTrue/False, assertNull/NotNull, assertSame/NotSame, fail 등
  • 예외 검증
    • assertThrows, assertDoesNotThrow + Executable 함수형 인터페이스
  • assertAll
    • 여러 검증을 람다로 묶어 한 번에 실행, 실패 목록을 모두 보여줌
  • 테스트 라이프사이클
    • 각 테스트마다 인스턴스 생성 → @BeforeEach → @Test → @AfterEach
    • 전체 전/후로 @BeforeAll / @AfterAll 한 번씩 실행
  • 독립적인 테스트
    • 실행 순서 가정 X
    • 필드 공유/상태 의존 X
  • 편의 애노테이션
    • @DisplayName – 사람이 읽기 좋은 이름
    • @Disabled – 일시 비활성화
  • 모든 테스트 실행
    • Maven/Gradle/IDE에서 프로젝트 전체 테스트 실행 습관 들이기