단위 테스트(Unit Test)를 처음 작성하다 보면 기능 구현보다 테스트 메서드 이름 짓기에서 더 많은 고민을 하게 되는 경우가 있다.
Roy Osherove의 The Art of Unit Testing 과 Ajitesh Kumar의 "7 Popular Unit Test Naming Conventions" 등 유명한 자료들을 바탕으로 정리했다.
테스트 이름은 단순한 레이블이 아니다. 테스트가 실패했을 때, 코드를 열어보지 않고도 무엇이 왜 실패했는지 파악할 수 있어야 한다. 그것이 좋은 테스트 이름의 핵심이다.
// 나쁜 예
@Test
void test1() { ... }
@Test
void withdrawTest() { ... }
// 좋은 예
@Test
void withdraw_InsufficientBalance_ThrowsException() { ... }
테스트가 수백 개 쌓였을 때, CI/CD 빌드 서버의 로그에서 실패한 테스트를 추적해야 한다고 생각해보자. 이름 하나가 디버깅 시간을 크게 단축시켜 줄 수 있다.
대부분의 네이밍 컨벤션은 아래 3가지 요소를 어떤 순서, 어떤 형태로 배치하느냐의 차이다.
| 요소 | 의미 | 예시 |
|---|---|---|
| Unit of Work | 테스트 대상 메서드/기능 | withdraw |
| State Under Test | 테스트 시점의 상태/조건 | InsufficientBalance |
| Expected Behavior | 기대하는 결과 | ThrowsException |
아래 예시는 모두 동일한 시나리오를 다룬다.
시나리오: 계좌 잔액이 부족할 때 출금을 시도하면 예외가 발생해야 한다.
MethodName_StateUnderTest_ExpectedBehavior
Roy Osherove가 The Art of Unit Testing에서 제안한 방식으로, 가장 널리 알려진 컨벤션이다.
@Test
void withdraw_InsufficientBalance_ThrowsException() {
// Arrange
Account account = new Account(100);
// Act & Assert
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
장점: 메서드명과 상태, 결과가 명확히 분리된다.
단점: 리팩토링으로 메서드 이름이 바뀌면 테스트 이름도 함께 갱신해야 한다.
MethodName_ExpectedBehavior_StateUnderTest
1번의 순서를 약간 바꾼 변형이다. 일부 팀에서는 결과를 먼저 보고 싶을 때 이 순서를 선호한다.
@Test
void withdraw_ThrowsException_WhenBalanceIsInsufficient() {
Account account = new Account(100);
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
단점: 1번과 동일하게 메서드명 변경 시 유지보수 부담이 있다.
test[FeatureBeingTested]
테스트 프레임워크의 어노테이션(@Test)이 이미 "이것은 테스트다"라는 것을 나타내므로, 메서드 이름에는 기능 자체만 담는다는 철학이다.
@Test
void testWithdrawWithInsufficientBalance() {
Account account = new Account(100);
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
참고: JUnit 5부터는 test 접두사 없이도 @Test만으로 충분하므로, 이 방식의 test 접두사는 다소 중복된 표현으로 여겨지기도 한다.
FeatureBeingTested(접두사 없음)
3번에서 test 접두사조차 제거한 방식이다. JUnit 5의 @DisplayName과 조합하면 매우 읽기 좋은 테스트 결과를 만들 수 있다.
@Test
@DisplayName("잔액이 부족한 경우 출금 시 예외가 발생한다")
void withdrawWithInsufficientBalance() {
Account account = new Account(100);
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
JUnit 5의 @DisplayName은 한글 설명도 가능하므로, 메서드명은 간결하게 유지하면서 가독성을 높이는 방식으로 많이 활용된다.
Should_ExpectedBehavior_When_StateUnderTest
"~해야 한다"는 표현으로 요구사항을 자연어에 가깝게 서술하는 방식이다.
@Test
void Should_ThrowException_When_BalanceIsInsufficient() {
Account account = new Account(100);
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
장점: 사람이 읽기 자연스럽고, 요구사항 문서처럼 읽힌다.
When_StateUnderTest_Expect_ExpectedBehavior
"~한 상태일 때, ~를 기대한다"는 구조로, 조건이 먼저 오는 형태다.
@Test
void When_BalanceIsInsufficient_Expect_ExceptionThrown() {
Account account = new Account(100);
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
Given_Preconditions_When_StateUnderTest_Then_ExpectedBehavior
BDD(Behavior-Driven Development) 의 Given-When-Then 패턴에서 유래한 방식이다. 가장 서술적이며, 비개발자도 이해하기 쉬운 구조를 지향한다.
@Test
void Given_AccountWith100_When_WithdrawingMore_Then_ThrowsException() {
// Given
Account account = new Account(100);
// When & Then
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
장점: 테스트가 요구사항 명세서처럼 기능한다. Cucumber, JBehave 같은 BDD 프레임워크와 철학적으로 잘 맞는다.
단점: 이름이 길어질 수 있다.
| # | 패턴 | 스타일 | 추천 상황 |
|---|---|---|---|
| 1 | Method_State_Behavior | 기술적 | 일반적인 단위 테스트, 레거시 코드베이스 |
| 2 | Method_Behavior_State | 기술적 | 결과 중심 팀 |
| 3 | testFeature | 단순 | 소규모 프로젝트 |
| 4 | featureDescription | 간결 | JUnit 5 + @DisplayName 조합 |
| 5 | Should_Behavior_When_State | 서술적 | 요구사항 중심 개발 |
| 6 | When_State_Expect_Behavior | 서술적 | 조건 중심 서술 선호 팀 |
| 7 | Given_When_Then | BDD | BDD 팀, 기획자와 협업 시 |
이미 구축된 레거시 코드베이스에서는 팀 내 기존 컨벤션을 따르는 것이 최우선이다. 새 프로젝트라면 아래 기준으로 선택을 권장한다.
순수 Java 백엔드, 팀 내 개발자만 보는 경우 → 패턴 1 또는 4 (@DisplayName 활용)
BDD를 도입하거나, 비개발자도 테스트 명세를 확인하는 경우 → 패턴 7
Spring Boot 기반 REST API 테스트 → 패턴 5 또는 7
위 7가지 중 현재 Java/Spring Boot 생태계에서 가장 많이 보이는 패턴은 1번과 4번(+ @DisplayName)
가장 중요한 원칙은 팀 내 일관성이다. 어떤 컨벤션을 선택하든, 프로젝트 전체에서 동일하게 적용하는 것이 무엇보다 우선이다.
@DisplayName 적극 활용@Nested
@DisplayName("출금(withdraw) 기능")
class WithdrawTest {
@Test
@DisplayName("잔액이 부족하면 예외를 던진다")
void throwsExceptionWhenBalanceInsufficient() {
Account account = new Account(100);
assertThrows(InsufficientBalanceException.class,
() -> account.withdraw(200));
}
@Test
@DisplayName("잔액이 충분하면 출금 후 잔액이 감소한다")
void decreasesBalanceWhenSufficientFunds() {
Account account = new Account(100);
account.withdraw(50);
assertEquals(50, account.getBalance());
}
}
JUnit 5의 @Nested + @DisplayName 조합은 계층적 테스트 구조를 만들 수 있어, 메서드 이름의 가독성 부담을 크게 줄여준다. 현재 Java 생태계에서 가장 권장되는 방식 중 하나이다.
// JUnit 기본 assert 대신
assertEquals(50, account.getBalance());
// AssertJ 스타일 (더 읽기 쉬움)
assertThat(account.getBalance()).isEqualTo(50);
assertThat(account.getBalance()).isGreaterThan(0).isLessThan(100);
네이밍과 더불어, AssertJ를 활용하면 테스트 코드 자체의 가독성도 크게 향상된다.
좋은 테스트 이름은 코드 문서화의 역할을 한다. 테스트가 실패했을 때 이름만 보고도 "무엇이, 어떤 조건에서, 어떻게 실패했는지"를 알 수 있다면 그것이 좋은 네이밍이다.
네이밍 컨벤션을 새로 도입한다면, 팀원들과 합의하여 하나를 선택하고 일관되게 적용하는 것을 권장한다.