단위테스트 구조 및 Assertj 기초 정리

Seob·2025년 5월 28일

개념정리

목록 보기
4/4
post-thumbnail

도입

김영한님의 Spring 강의와 Java/Spring 테스트를 추가하고 싶은 개발자들의 오답노트 강의를 들으면서 실제 프로젝트에 적용할 때 정리해두면 좋을 내용들을 정리해봤다.
단위 테스트는 코드의 품질을 보장하고 리팩토링의 안전망 역할을 하는 중요한 요소라고 할 수 있다.

1. 단위 테스트란?

단위 테스트(Unit Test)란 하나의 '단위'에 해당하는 클래스나 메서드를 외부 의존성 없이 독립적으로 테스트하는 것을 말한다. 일반적으로는 도메인 로직, 유틸성 메서드 등을 테스트하며, 외부 시스템(DB, 네트워크 등)은 포함하지 않는다.

단위 테스트 특징:

  • 속도 빠름
  • 신뢰성(실행에 따른 결과가 변하지 않음)
  • 단일 서버, 단일 프로세스, 단일 스레드
  • 한 번에 하나의 기능(단위)만 검증

통합 테스트와의 차이

항목단위 테스트통합 테스트
테스트 범위하나의 클래스/메서드여러 컴포넌트 간의 상호작용
실행 속도매우 빠름상대적으로 느림
외부 의존성없음 (또는 Mock 사용)DB, 네트워크 등 실제 시스템 포함
테스트 목적개별 로직 검증시스템 간 연결 및 전체 흐름 검증

단위 테스트는 작은 단위를 신뢰할 수 있게 만드는 작업이고, 통합 테스트는 그 단위들이 잘 연결되는지를 검증하는 작업이다.

2. 단위 테스트의 기본 구조

단위 테스트를 시행할 때, 가장 널리 사용되는 패턴이 Given-When-Then 구조다.

@Test
@DisplayName("정상적인 이메일을 넣으면 Email 생성")
void validEmailTest() {
    // given - 데이터 설정
    String emailText = "hyeonseob22@gmail.com";
    
    // when - 실행
    Email email = Email.from(emailText);
    
    // then - 결과 검증
    assertThat(email.getValue()).isEqualTo("hyeonseob22@gmail.com");
}

각 단계의 역할

  • Given: 테스트 데이터 준비, Mock 객체 설정, 전제 조건 구성
  • When: 실제로 테스트하려는 메서드나 동작 실행
  • Then: 결과 검증, 예상되는 상태나 동작 확인

단위 테스트는 위처럼 단일 스레드 기반에서 실행되며, 테스트 대상의 순수 로직만 집중해서 검증한다. 외부 의존성이 필요한 경우에는 Mock 등을 사용해 격리한다.

3. AssertJ - 더 나은 테스트 검증

AssertJ는 JUnit의 기본 assertion보다 가독성이 뛰어나고 더 많은 기능을 제공하는 라이브러리다.

기본 설정 (Gradle)

Spring Boot 프로젝트라면 spring-boot-starter-test에 AssertJ가 이미 포함되어 있다:

testImplementation 'org.springframework.boot:spring-boot-starter-test'

Spring Boot 없이 사용하는 경우:

testImplementation 'org.assertj:assertj-core:3.24.2'
import static org.assertj.core.api.Assertions.*;

JUnit vs AssertJ 비교

JUnit 방식:

@Test
void userValidationTest() {
    User user = new User("", 15);
    
    assertFalse(user.getName().isEmpty()); // 실패 시: Expected: <false> but was: <true>
    assertTrue(user.getAge() >= 18);       // 실패 시: Expected: <true> but was: <false>
}

AssertJ 방식 (권장):

@Test
void userValidationTest() {
    User user = new User("", 15);
    
    assertThat(user.getName())
        .isNotEmpty()
        .hasSize(5);
        
    assertThat(user.getAge())
        .isGreaterThanOrEqualTo(18);
}

에러 메시지 비교

  • JUnit: Expected: <true> but was: <false> → 무엇을 검증하려 했는지 불명확
  • AssertJ: Expecting actual:<15> to be greater than or equal to:<18> → 구체적이고 명확함

AssertJ 컬렉션 검증

컬렉션 테스트에서 AssertJ의 진가가 발휘된다.

@Test
@DisplayName("사용자 목록 필터링 및 검증")
void userListFilteringTest() {
    // given
    List<User> users = Arrays.asList(
        new User("alice@gmail.com", "Alice", 25, true),
        new User("bob@gmail.com", "Bob", 30, false),
        new User("charlie@gmail.com", "Charlie", 35, true)
    );

    // when
    List<User> activeUsers = users.stream()
        .filter(User::isActive)
        .collect(toList());

    // then
    assertThat(activeUsers)
        .hasSize(2)
        .extracting(User::getName)
        .containsOnly("Alice", "Charlie")
        .doesNotContain("Bob");
        
    assertThat(users)
        .filteredOn(User::isActive)
        .extracting("name", "age")
        .containsExactly(
            tuple("Alice", 25),
            tuple("Charlie", 35)
        );
}

주요 컬렉션 검증 메서드들

  • hasSize(int) - 컬렉션 크기 검증
  • isEmpty() / isNotEmpty() - 빈 컬렉션 여부
  • contains(Object...) - 지정된 요소들 포함 여부 (순서 무관)
  • containsOnly(Object...) - 정확히 지정된 요소들만 포함 (순서 무관)
  • containsExactly(Object...) - 정확히 지정된 요소들만 순서대로 포함
  • doesNotContain(Object...) - 지정된 요소들 미포함
  • extracting(String...) - 객체의 특정 필드들 추출
  • extracting(Function) - 함수를 통한 값 추출
  • filteredOn(Condition) - 조건에 맞는 요소들로 필터링
  • allMatch(Predicate) / anyMatch(Predicate) - 모든/일부 요소 조건 만족

4. 일반적인 테스트 요소들

4.1 객체 생성 및 상태 검증

@Test
@DisplayName("User 생성")
void create_UserDomain() {
    // given
    String email = "test@gmail.com";
    String nickname = "nickname";
    String rawPassword = "Password123!";
    
    // when
    UserDomain userDomain = UserDomain.create(email, nickname, rawPassword);

    // then
    assertThat(userDomain).isNotNull();
    assertThat(userDomain.getEmail().getValue()).isEqualTo("test@gmail.com");
    assertThat(userDomain.getNickname()).isEqualTo("nickname");
    assertThat(userDomain.isActive()).isFalse(); // 기본값 검증
}

4.2 예외 상황 테스트

예외 테스트는 단순히 예외가 발생하는지 확인하는 것을 넘어서 구체적인 예외 메시지까지 검증하는 것이 좋다.
간단한 타입 검증의 경우에는 isInstanceOf(Class) 까지만 사용하지만 구체적인 에러 메시지가 필요한 경우 hasMessage()등을 사용해서 체크한다.

@Test
@DisplayName("이메일이 100자가 넘어갈 경우 예외가 발생")
void emailOver100Test() {
    // given
    String longEmailTest = "h".repeat(101) + "@gmail.com";

    // when & then
    assertThatThrownBy(() -> Email.from(longEmailTest))
        .isInstanceOf(InvalidEmailFormatException.class) // 간단한 예외 처리일 경우 여기까지만 해도 됨
        .hasMessageContaining("100자")
        .hasMessageContaining("초과");
}

@Test
@DisplayName("잘못된 이메일 형식으로 생성 시 정확한 예외 메시지")
void invalidEmailFormatTest() {
    // given
    String invalidEmail = "invalid-email";

    // when & then
    assertThatThrownBy(() -> Email.from(invalidEmail))
        .isInstanceOf(InvalidEmailFormatException.class)
        .hasMessage("올바른 이메일 형식이 아닙니다: " + invalidEmail);
}

@Test
@DisplayName("비밀번호가 8자 미만일 때 예외")
void passwordTooShortTest() {
    // given
    String shortPassword = "123";

    // when & then
    assertThatThrownBy(() -> Password.from(shortPassword))
        .isInstanceOf(InvalidPasswordException.class) 
        .hasMessageStartingWith("비밀번호는")
        .hasMessageEndingWith("이상이어야 합니다");
}

예외 메시지 검증 메서드들

  • hasMessage(String) - 정확한 메시지 일치
  • hasMessageContaining(String) - 메시지에 특정 문자열 포함
  • hasMessageStartingWith(String) - 메시지 시작 부분
  • hasMessageEndingWith(String) - 메시지 끝 부분
  • hasMessageMatching(String regex) - 정규표현식 패턴
  • hasNoCause() - 원인 예외가 없음을 검증
  • hasCauseInstanceOf(Class) - 특정 타입의 원인 예외 검증

4.3 동등성 검증

@Test
@DisplayName("같은 이메일 값이라면 equals/hashCode 결과 동일")
void equalsAndHashCodeTest() {
    // given
    String emailValue = "test@gmail.com";

    // when
    Email email1 = Email.from(emailValue);
    Email email2 = Email.from(emailValue);

    // then
    assertThat(email1).isEqualTo(email2);
    assertThat(email1.hashCode()).isEqualTo(email2.hashCode());
}

동등성과 해시코드는 VO에서 매우 중요하다. 특히 Set, Map 같은 자료구조를 사용할 때 반드시 필요한 검증이다.

마무리

단위 테스트는 코드의 품질을 보장하고 안전한 리팩토링을 가능하게 하는 필수 요소다.

핵심 내용:

  • AssertJ 활용: JUnit 기본 assertion보다 가독성이 뛰어나고 구체적인 에러 메시지 제공
  • 예외 검증: 예외 발생 여부뿐만 아니라 메시지까지 꼼꼼히 검증하여 버그 사전 방지
  • 컬렉션 검증: extracting(), filteredOn(), containsOnly() 등으로 복잡한 컬렉션도 간단히 검증
  • 테스트 독립성: 각 테스트는 다른 테스트에 의존하지 않고 독립적으로 실행되어야 함
  • 단일 관심사: 하나의 테스트는 하나의 기능이나 동작만 검증하여 명확성 유지
  • 가독성: 테스트 코드도 운영 코드만큼 중요하므로 명확하고 이해하기 쉽게 작성

이러한 원칙들을 지키면서 테스트를 작성하면, 유지보수하기 쉽고 신뢰할 수 있는 코드를 만들 수 있다.

profile
백엔드 개발자 Seob입니다

0개의 댓글