본문 바로가기

SPRING/Spring

[스프링| 스프링 입문 | 코드로 배우는 스프링] 회원 서비스 테스트

package hello.hellospring.service;  // 패키지 선언

import hello.hellospring.domain.Member;  // Member 도메인 클래스를 가져옵니다.
import hello.hellospring.repository.MemoryMemberRepository;  // 회원 정보를 메모리에 저장하는 레포지토리 클래스를 가져옵니다.
import org.assertj.core.api.Assertions;  // assertj의 Assertions를 사용하여 더 읽기 쉽고 강력한 검증을 제공합니다.
import org.junit.jupiter.api.AfterEach;  // 각 테스트가 끝날 때마다 실행할 메소드를 지정합니다.
import org.junit.jupiter.api.BeforeEach;  // 각 테스트가 시작될 때마다 실행할 메소드를 지정합니다.
import org.junit.jupiter.api.Test;  // 테스트 메소드임을 표시합니다.

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;  // 테스트 검증을 위해 assertThat을 정적으로 임포트합니다.
import static org.junit.jupiter.api.Assertions.*;  // JUnit의 Assertions 클래스의 정적 메소드를 가져옵니다.

class MemberServiceTest {  // 테스트 클래스를 선언합니다.

    MemberService memberService;  // MemberService 객체를 선언합니다.
    MemoryMemberRepository memberRepository;  // MemoryMemberRepository 객체를 선언합니다.

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();  // 테스트 시작 전에 새로운 MemoryMemberRepository를 생성합니다.
        memberService = new MemberService(memberRepository);  // memberService를 초기화하면서 memberRepository를 주입합니다.
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();  // 테스트가 끝날 때마다 저장소를 초기화합니다.
    }

    @Test
    void 회원가입() {
        //given
        Member member = new Member();  // 새 Member 객체를 생성합니다.
        member.setName("hello");  // Member 객체의 이름을 'hello'로 설정합니다.

        //when
        Long saveId = memberService.join(member);  // memberService를 통해 회원을 가입시키고 ID를 저장합니다.

        //then
        Member findMember = memberService.findOne(saveId).get();  // 저장된 ID로 회원을 검색합니다.
        assertThat(member.getName()).isEqualTo(findMember.getName());  // 저장된 회원의 이름이 입력한 이름과 같은지 확인합니다.
    }

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();  // 첫 번째 Member 객체를 생성합니다.
        member1.setName("spring");  // 이름을 'spring'으로 설정합니다.

        Member member2 = new Member();  // 두 번째 Member 객체를 생성합니다.
        member2.setName("spring");  // 이름을 'spring'으로 설정합니다. (중복 이름)

        //when
        memberService.join(member1);  // 첫 번째 회원을 가입시킵니다.
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));  // 두 번째 회원 가입 시 예외를 기대합니다.

        //then
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");  // 예외 메시지가 올바른지 확인합니다.
    }
}

@BeforeEach

@BeforeEach는 JUnit 테스트 프레임워크에서 사용되는 어노테이션입니다. 이 어노테이션을 메소드에 적용하면 해당 테스트 클래스 내에 있는 각각의 테스트 메소드가 실행되기 전에 @BeforeEach가 적용된 메소드가 먼저 실행됩니다. 이를 통해 각 테스트 실행 전에 필요한 초기 설정이나 준비 작업을 수행할 수 있습니다. 이런 방식은 테스트 간에 데이터 간섭을 방지하고, 각 테스트가 독립적인 환경에서 실행될 수 있도록 보장합니다.

 

DI(의존성주입)

코드 내에서 memberRepository = new MemoryMemberRepository();는 새로운 MemoryMemberRepository 인스턴스를 생성하고 있습니다. MemoryMemberRepository는 메모리 기반의 저장소를 제공하여, 데이터베이스나 외부 저장소에 의존하지 않고 테스트를 수행할 수 있게 합니다. 이는 테스트 속도를 높이고 외부 요인으로 인한 테스트 실패 가능성을 줄입니다.

memberService = new MemberService(memberRepository);는 생성된 memberRepository 인스턴스를 MemberService의 생성자로 전달하여 MemberService 객체를 초기화하는 코드입니다. 이 과정은 의존성 주입(Dependency Injection, DI)의 한 예로 볼 수 있습니다. 여기서 의존성 주입은 MemberService가 외부에서 MemoryMemberRepository 객체를 받아 자신의 의존 객체로 설정하는 것을 말합니다. 이 방법은 MemberService의 테스트 가능성과 유연성을 향상시키며, MemberService가 특정 저장소 구현에 강하게 결합되는 것을 방지합니다.

이러한 의존성 주입은 다음과 같은 이점을 제공합니다:

  1. 테스트 용이성: MemberService가 구체적인 저장소 구현이 아닌 인터페이스에 의존하므로, 테스트 시에 가짜(Mock) 또는 스텁(Stub) 구현을 주입하여 테스트를 용이하게 할 수 있습니다.
  2. 코드의 유연성: 저장소의 구현을 변경하더라도 MemberService 코드를 수정할 필요가 없으며, 다른 종류의 저장소 구현체를 쉽게 교체할 수 있습니다.
  3. 결합도 감소: MemberService는 저장소의 구체적인 구현보다는 인터페이스에 의존함으로써 낮은 결합도를 유지할 수 있어, 시스템의 전체적인 유지보수성이 향상됩니다.

이처럼 @BeforeEach를 통해 설정된 의존성 주입은 각 테스트가 깨끗한 상태에서 시작될 수 있도록 하며, 테스트 코드의 신뢰성과 유지 관리성을 높이는 데 기여합니다.

 

Given

이 섹션은 테스트에 필요한 사전 조건이나 초기 설정을 구성합니다. 테스트의 "상황 설정" 부분에 해당합니다. 여기서는 테스트할 데이터를 생성하고 초기 상태를 설정합니다.

  • Member member = new Member(); - Member 클래스의 새 인스턴스를 생성합니다.
  • member.setName("hello"); - 생성한 Member 객체의 이름을 "hello"로 설정합니다.

When

이 섹션은 테스트의 실행 부분으로, 실제로 테스트하고자 하는 기능 또는 메소드를 실행합니다. "테스트 행동" 부분에 해당하며, 이 결과를 기반으로 검증을 수행합니다.

  • Long saveId = memberService.join(member); - MemberService의 join 메소드를 호출하여, 주어진 Member 객체를 회원 가입시키고, 저장된 회원의 ID를 반환받습니다.

Then

이 섹션은 테스트의 검증 부분으로, 실행 결과가 기대하는 바와 일치하는지 확인합니다. "결과 검증" 부분으로, 이 섹션의 검증을 통해 테스트의 성공 여부를 판단합니다.

  • Member findMember = memberService.findOne(saveId).get(); - memberService를 통해 saveId로 회원 정보를 검색합니다.
  • assertThat(member.getName()).isEqualTo(findMember.getName()); - assertThat을 사용하여 검색된 회원의 이름이 초기에 설정한 이름("hello")과 동일한지 확인합니다.

assertThat()

assertThat() 메소드는 assertj 라이브러리의 일부로, 테스트의 검증을 위해 사용됩니다. 이 메소드는 플루언트 인터페이스를 제공하며, 읽기 쉽고 사용자 친화적인 검증 메시지를 생성할 수 있게 해줍니다. assertThat()은 검증 대상을 인자로 받아 Assertion 객체를 반환하고, 이 객체는 다양한 체이닝 검증 메소드(isEqualTo, isTrue, contains 등)를 제공합니다. 위 코드에서는 isEqualTo 메소드를 사용하여 기대값과 실제 값을 비교합니다.

 

중복 회원 예외 처리

"중복 회원 예외"를 처리하는 로직을 검증하기 위해 작성된 것입니다. 회원 관리 시스템에서 중복된 이름의 회원이 가입하려 할 때 시스템이 어떻게 반응하는지 확인하는 것이 목적입니다. 테스트는 다음과 같은 세 부분으로 구성됩니다: Given, When, Then. 여기서는 assertThrows 메서드를 통해 예외 처리 테스트를 진행하고 있습니다.

Given

이 부분에서는 테스트의 전제 조건을 설정합니다.

  • Member member1 = new Member(); - 첫 번째 Member 객체를 생성합니다.
  • member1.setName("spring"); - 첫 번째 회원의 이름을 "spring"으로 설정합니다.
  • Member member2 = new Member(); - 두 번째 Member 객체를 생성합니다. 이 객체는 중복 테스트를 위해 동일한 이름("spring")을 가집니다.

When

이 부분에서는 실제로 테스트할 로직을 실행합니다.

  • memberService.join(member1); - 첫 번째 회원을 정상적으로 가입 처리합니다.
  • IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2)); - 두 번째 회원을 가입시킬 때 IllegalStateException 예외가 발생하는지 확인합니다. assertThrows 메서드는 첫 번째 인자로 예상되는 예외 클래스를 받고, 두 번째 인자로는 예외를 발생시킬 수 있는 실행 블록을 람다 표현식으로 제공합니다.

Then

이 부분에서는 실행 결과를 검증합니다.

  • assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); - 발생한 예외의 메시지가 "이미 존재하는 회원입니다."인지 확인합니다. 이 메시지는 중복 회원 가입 시도를 정확히 잡아내고 올바른 예외 메시지를 반환하는지 검증합니다.

예외처리 테스트 방법

예외 처리 테스트는 일반적인 테스트와 다르게, 코드가 예외 상황에서도 올바르게 동작하는지 확인하는 것을 목표로 합니다. JUnit에서는 assertThrows 메서드를 사용하여 예외 처리를 테스트합니다. 이 메서드는 다음과 같은 방식으로 작동합니다:

  1. 예상되는 예외 타입을 지정합니다. 첫 번째 매개변수로 예외 클래스를 넣어서 어떤 종류의 예외를 기대하는지 명시합니다.
  2. 실행할 코드 블록을 제공합니다. 람다 표현식을 통해 예외를 유발할 수 있는 코드를 실행합니다.
  3. 발생한 예외를 반환합니다. 예외가 발생하면 그 예외를 반환하고, 그렇지 않으면 테스트가 실패합니다.
  4. 검증: 반환된 예외를 검사하여 예외의 유형이나 메시지가 기대치와 일치하는지 확인합니다.
public void 중복_회원_예외() {
    //given
    Member member1 = new Member();  // 첫 번째 Member 객체를 생성합니다.
    member1.setName("spring");  // 이름을 'spring'으로 설정합니다.

    Member member2 = new Member();  // 두 번째 Member 객체를 생성합니다.
    member2.setName("spring");  // 이름을 'spring'으로 설정합니다. (중복 이름)

    //when
    memberService.join(member1);  // 첫 번째 회원을 가입시킵니다.
    
    // then
    try {
        memberService.join(member2);  // 두 번째 회원 가입 시도
        fail("예외가 발생해야 합니다.");  // 만약 예외가 발생하지 않는다면 테스트 실패
    } catch (IllegalStateException e) {
        // 예외 메시지가 올바른지 확인합니다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }
}
  • try 블록: 이 블록 안에서 예외가 발생할 수 있는 코드를 실행합니다. 여기서는 memberService.join(member2);가 중복 회원 가입을 시도하며, 이 때 IllegalStateException이 발생할 것으로 예상합니다.
  • catch 블록: try 블록 내에서 발생한 예외를 처리합니다. 만약 IllegalStateException이 발생하면, 이 블록이 실행됩니다. 여기서 예외의 메시지를 검증하여 올바른 예외가 발생했는지 확인할 수 있습니다.
  • fail 메소드: 예외가 발생하지 않을 경우, 즉 memberService.join(member2); 호출이 정상적으로 처리될 경우 fail() 메소드가 호출되어 테스트가 실패하도록 합니다. 이는 예외가 발생해야 하는 상황에서 예외가 발생하지 않았음을 나타냅니다.