테스트

JUnit 기초

코드를 고치면 "잘 동작하던 게 망가지지 않았나" 걱정된다. 매번 브라우저를 열어 손으로 확인하는 대신, 검증 코드를 한 번 작성해 두고 버튼 한 번으로 반복 실행하는 것이 테스트 코드다. 자바에서는 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

클래스 전체에서 한 번 (static)

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

한 테스트가 남긴 데이터가 다음 테스트에 영향을 주면, 실행 순서에 따라 결과가 달라진다. @BeforeEach로 매번 초기화해 각 테스트가 독립적으로 돌아가게 만든다.


6. 무엇을, 언제 테스트하는가

"메서드마다 하나씩 만드나? 모든 코드를 테스트해야 하나?" — 가장 흔한 고민이다. 핵심 원칙은 하나다.

메서드가 아니라 "행동(behavior)"을 테스트한다.

add() 메서드가 있다고 add 테스트 하나를 만드는 게 아니라, "이 상황에서는 이렇게 동작해야 한다"는 규칙마다 테스트를 만든다. 그래서 한 메서드에 테스트가 여러 개 나오기도 하고, 로직이 없는 메서드엔 하나도 없을 수 있다.

무엇을 테스트하나 — 우선순위

우선순위

대상

이유

높음

비즈니스 로직·규칙 (할인 계산, 가입 검증, 상태 전이)

틀리면 바로 사고로 이어짐. 테스트 가치가 가장 큼

높음

분기·경계·예외 (if/else, 0·음수·null, 한곗값, 예외 조건)

버그가 숨는 곳. 상황별로 나눠 확인

중간

버그가 났던 코드

같은 버그 재발 방지(회귀 테스트)

낮음·생략

단순 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로 모아 각 테스트가 독립적으로 실행되게 한다.

  • 다음 장에서 이 기본기를 바탕으로 단위 테스트와 통합 테스트, 그리고 스프링과 함께 쓰는 법을 본다.

마지막 수정 2026년 10월 22일