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 초기화 등
라이프사이클 순서는
- 테스트 클래스 인스턴스 생성
- @BeforeEach 메서드 실행
- @Test 메서드 실행
- @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에서 프로젝트 전체 테스트 실행 습관 들이기
'외부활동 > 우아한테크코스 [프리코스]' 카테고리의 다른 글
| 테스트주도 개발 시작하기 - 챕터 7. 대역 (0) | 2025.11.06 |
|---|---|
| 테스트주도 개발 시작하기 - 챕터 6. 테스트 코드 구성 (0) | 2025.11.05 |
| 테스트주도 개발 시작하기 - 챕터 4. TDD, 기능명세, 설계 (0) | 2025.11.05 |
| 테스트주도 개발 시작하기 - 챕터 3. 테스트 코드 작성 순서 (0) | 2025.11.04 |
| 테스트주도 개발 시작하기 - 챕터 2. TDD 시작 (0) | 2025.11.04 |