코드를 고치면 "잘 동작하던 게 망가지지 않았나" 걱정된다. 매번 브라우저를 열어 손으로 확인하는 대신, 검증 코드를 한 번 작성해 두고 버튼 한 번으로 반복 실행하는 것이 테스트 코드다. 자바에서는 JUnit이 사실상 표준이며, 스프링 부트는 spring-boot-starter-test 안에 JUnit 5를 기본 포함한다.
이 챕터에 대하여
Spring MVC — 의존성 주입 설명에서 "생성자 주입이라야 테스트가 쉽다"는 이야기가 나온다. 그 말을 제대로 이해하려면 테스트 코드를 읽고 쓸 줄 알아야 해서 이 챕터를 둔다. 여기서 기본기를 익히고, 다음 장에서 스프링과 함께 쓰는 법을 본다.
1. 왜 테스트 코드를 작성하는가
손으로 확인 | 테스트 코드 |
|---|---|
매번 앱을 띄우고 클릭해 확인 | 버튼 하나로 수십 개를 몇 초 만에 검증 |
고친 뒤 다른 기능이 깨졌는지 모름 | 깨지면 빨간불로 즉시 알려 줌(회귀 방지) |
기록이 남지 않음 | 테스트 자체가 "이 코드는 이렇게 동작한다"는 문서가 됨 |
테스트가 곧 설계 피드백
테스트하기 어려운 코드는 대개 설계가 엉켜 있다는 신호다. 예컨대 의존성을 바깥에서 넣어줄 통로가 없으면(=필드 주입) 테스트가 막힌다. 그래서 "테스트가 쉬운 구조"는 곧 "좋은 구조"인 경우가 많다.
2. 테스트 코드는 어디에 두는가
스프링 부트(Gradle/Maven) 프로젝트는 운영 코드와 테스트 코드의 디렉토리가 나뉘어 있다.
src/
├── main/java/... # 운영 코드
└── test/java/... # 테스트 코드 (여기에 작성)보통 테스트 클래스 이름은 대상클래스 + Test 로 짓는다. 예: Calculator → CalculatorTest.
3. 첫 번째 테스트 — @Test
순수 자바 클래스를 하나 테스트해 보자.
// src/main/java — 테스트 대상
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}// src/test/java — 테스트 코드
import org.junit.jupiter.api.Test; // JUnit 5
import static org.junit.jupiter.api.Assertions.*; // assertEquals 등
class CalculatorTest {
@Test // 이 메소드가 "테스트"임을 표시
void 두_수를_더한다() {
// given - 준비
Calculator calc = new Calculator();
// when - 실행
int result = calc.add(2, 3);
// then - 검증
assertEquals(5, result); // 기대값 5 == 실제값 result?
}
}@Test — 붙은 메소드를 JUnit이 테스트로 인식해 실행한다.
given–when–then — 준비 → 실행 → 검증의 3단계로 본문을 나누면 의도가 또렷해진다.
검증이 통과하면 초록불, 기대와 다르면 빨간불로 표시된다.
IDE에서 실행
IntelliJ 등에서는 테스트 메소드/클래스 왼쪽의 ▶ 버튼으로 바로 실행한다. 터미널에서는 ./gradlew test(Gradle) 또는 mvn test(Maven)로 전체 테스트를 돌린다.
4. 검증 — Assertions
검증(assertion)은 "이 값이 이래야 한다"를 코드로 단언하는 것이다. 단언이 거짓이면 테스트는 실패한다.
assertEquals(5, result); // 같은가
assertTrue(member.isActive()); // 참인가
assertNull(found); // null인가
assertThrows(IllegalArgumentException.class,
() -> calc.divide(1, 0)); // 예외가 터지는가AssertJ — 더 읽기 좋은 검증
스프링 부트 테스트에는 AssertJ도 함께 들어 있다. assertThat(...).isEqualTo(...) 처럼 영어 문장처럼 읽혀 실무에서 선호된다.
import static org.assertj.core.api.Assertions.assertThat;
assertThat(result).isEqualTo(5);
assertThat(members).hasSize(3).contains(memberA);5. 생명주기 — @BeforeEach / @AfterEach
여러 테스트가 공통으로 쓰는 준비 작업은 @BeforeEach로 모은다. 각 테스트 실행 직전마다 호출되어, 테스트끼리 상태가 섞이지 않게 해 준다.
class MemberServiceTest {
MemberService service;
@BeforeEach // 각 @Test 실행 전마다 새로 준비
void setUp() {
service = new MemberService(new MemoryMemberRepository());
}
@Test
void 회원_가입() { ... } // setUp() 후 실행
@Test
void 중복_회원_예외() { ... } // 다시 setUp() 후 실행 (독립 상태)
}어노테이션 | 호출 시점 |
|---|---|
@BeforeEach | 각 테스트 메소드 전 |
@AfterEach | 각 테스트 메소드 후 (정리 작업) |
@BeforeAll / @AfterAll | 클래스 전체에서 한 번 ( |
테스트는 서로 독립적이어야 한다
한 테스트가 남긴 데이터가 다음 테스트에 영향을 주면, 실행 순서에 따라 결과가 달라진다. @BeforeEach로 매번 초기화해 각 테스트가 독립적으로 돌아가게 만든다.
6. 무엇을, 언제 테스트하는가
"메서드마다 하나씩 만드나? 모든 코드를 테스트해야 하나?" — 가장 흔한 고민이다. 핵심 원칙은 하나다.
메서드가 아니라 "행동(behavior)"을 테스트한다.
add() 메서드가 있다고 add 테스트 하나를 만드는 게 아니라, "이 상황에서는 이렇게 동작해야 한다"는 규칙마다 테스트를 만든다. 그래서 한 메서드에 테스트가 여러 개 나오기도 하고, 로직이 없는 메서드엔 하나도 없을 수 있다.
무엇을 테스트하나 — 우선순위
우선순위 | 대상 | 이유 |
|---|---|---|
높음 | 비즈니스 로직·규칙 (할인 계산, 가입 검증, 상태 전이) | 틀리면 바로 사고로 이어짐. 테스트 가치가 가장 큼 |
높음 | 분기·경계·예외 ( | 버그가 숨는 곳. 상황별로 나눠 확인 |
중간 | 버그가 났던 코드 | 같은 버그 재발 방지(회귀 테스트) |
낮음·생략 | 단순 getter/setter, 로직 없는 위임, 단순 DTO | 검증할 규칙이 없음 |
생략 | 프레임워크·라이브러리 자체 동작 (JPA가 저장을 잘 하는지 등) | 내 코드가 아니라 이미 검증된 영역 |
커버리지 100%가 목표가 아니다
모든 줄을 테스트하려다 보면 가치 없는 테스트만 늘고 유지보수 부담이 커진다. 중요한 로직에 집중하는 편이 적은 테스트로 더 큰 안정성을 준다.
한 메서드, 여러 행동 — 예시
1만 원 이상이면 10% 할인하는 메서드를 보자.
public int discount(int price) {
if (price < 0) throw new IllegalArgumentException("가격은 음수일 수 없다");
return price >= 10_000 ? (int) (price * 0.1) : 0; // 1만원 이상만 10% 할인
}이 한 메서드에서 확인할 행동은 여러 개다. 정상·경계·예외를 각각 다른 테스트로 나눈다.
@Test
void 만원_미만은_할인_없음() {
assertThat(new Order().discount(9_999)).isEqualTo(0);
}
@Test
void 정확히_만원부터_할인_시작() { // 경계값
assertThat(new Order().discount(10_000)).isEqualTo(1_000);
}
@Test
void 만원_이상은_10퍼센트_할인() {
assertThat(new Order().discount(20_000)).isEqualTo(2_000);
}
@Test
void 음수_가격은_예외() { // 예외 상황
assertThatThrownBy(() -> new Order().discount(-1))
.isInstanceOf(IllegalArgumentException.class);
}이렇게 나누면 테스트가 깨졌을 때 어떤 상황이 틀렸는지 메서드 이름만 보고 바로 안다.
언제 작성하나
새 비즈니스 로직을 만들 때 — 규칙이 의도대로 동작하는지 확인하며 만든다.
버그를 고칠 때 — 먼저 그 버그를 재현하는 테스트를 써서 실패(빨간불)를 확인하고, 코드를 고쳐 통과(초록불)시킨다. 같은 버그의 재발을 막는다.
리팩터링 전 — 동작을 고정하는 테스트를 깔아 두면, 구조를 바꿔도 동작이 그대로임을 보장한다(안전망).
분기·계산이 복잡할 때 — 머리로 추적하기 어려운 곳일수록 테스트가 효자다.
TDD가 궁금하다면
"테스트를 먼저 쓰고(실패) → 코드를 고쳐 통과시키는" 흐름을 일반화한 것이 TDD(테스트 주도 개발) 다. 이 강의에서 필수는 아니지만, 위의 버그 재현 테스트 먼저 는 입문 단계에서도 바로 써먹을 수 있는 실용적인 습관이다.
정리
테스트 코드는 검증을 자동화해 회귀를 막고, 동시에 코드의 문서 역할을 한다.
테스트는 메서드가 아니라 행동(정상·경계·예외) 단위로, 비즈니스 로직을 우선해 작성한다.
테스트는
src/test/java에대상클래스Test이름으로 두고, 메소드에@Test를 붙인다.본문은 given–when–then으로 나누고, 결과는
assertEquals/assertThat으로 검증한다.공통 준비는
@BeforeEach로 모아 각 테스트가 독립적으로 실행되게 한다.다음 장에서 이 기본기를 바탕으로 단위 테스트와 통합 테스트, 그리고 스프링과 함께 쓰는 법을 본다.