본문 바로가기

Spring/스프링 입문

02. 회원 관리 페이지 만들기 - (5) 회원 서비스 테스트 만들기

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술에 대해 공부하고, 정리한 내용입니다.

1) 테스트 코드의 또 다른 생성 방법 

이전에는 Repository 기능을 테스트할 때, test package에 직접 repository package와 class를 생성했었습니다. 하지만 IDE의 기능을 활용하면 쉽게 test class를 생성할 수 있습니다.

 

생성 방법

1. 테스트하려는 클래스를 선택하면서 `Alt+Enter` 키를 누르고, `Create Test`를 선택합니다.

2. 테스트하려는 메서드를 선택하고 `OK`를 클릭합니다. 이렇게 하면 test package와 class가 자동으로 생성됩니다. 또한 선택한 메서드에는 `@Test` 어노테이션이 붙습니다. 이제 이 클래스에 테스트 코드를 작성하면 됩니다.

 

MemberService의 메서드 테스트

다음으로는 `MemberService`의 메서드를 테스트를 할 것 입니다. `MemberService`의 메서드를 테스트하기 위해서는 `MemberService` 객체가 필요합니다. 그러므로 `MemberService` 객체를 생성하겠습니다.

class MemberServiceTest {
    MemberService memberService;
}

이렇게 `MemberService` 객체를 생성하면, 이 객체를 통해 `MemberService`의 메서드를 테스트할 수 있습니다.


2) given, when, then 패턴

테스트 코드 작성 시, given-when-then 패턴은 자주 사용되는 구조입니다. 이 패턴은 테스트 코드의 목적과 구조를 명확하게 표현할 수 있습니다.

 

🔍 테스트 코드의 구조

  •  테스트 코드는 대체로 다음과 같은 구조를 가집니다: 어떤 상황이 주어졌을 때(given), 이것을 실행하면(when), 어떤 결과가 나와야 합니다(then).
  • 테스트 코드를 준비 - 실행 - 검증 이라는 세 부분으로 나누어 작성하면, 테스트 코드가 길어져도 각 부분을 쉽게 이해하고 관리할 수 있습니다.

 

📝 Given

  •  Given은 테스트를 위한 준비 과정입니다. 테스트에서 사용하는 변수, 입력 값들을 정의하거나, Mock 객체를 생성하는 부분이 이에 해당됩니다.

 

🎬 When

  •  When은 테스트를 실행하는 과정입니다. 이 과정에서는 하나의 메서드만 수행하는 것이 바람직합니다. When은 테스트의 핵심이지만, 코드의 길이는 가장 짧은 편입니다.

 

✅ Then

  •  Then은 테스트의 결과를 검증하는 과정입니다. 예상 값(expected)와 실제 값(actual)을 비교하여 테스트가 성공적으로 수행되었는지를 판단합니다. 주로 assertThat 구문을 사용하여 검증합니다.
  • 이렇게 given-when-then 패턴을 사용하면, 테스트 코드의 목적과 구조를 명확하게 표현하고, 테스트 코드를 쉽게 이해하고 관리할 수 있습니다.

3) 회원가입(Join) 테스트 코드 작성

 회원 가입을 처리하는 join 메서드에 대한 테스트를 작성해보겠습니다.

🔍 테스트 진행 방식

(1) Member 객체 member를 생성하여 이름을 "hello"로 설정합니다.

(2) 위에서 생성한 Member 객체를 join을 통해 회원 서비스에 가입시킵니다. join은 회원 가입 후, 회원의 id를 반환하므로 반환된 회원 id를 long형 변수 saveId에 저장합니다.

(3) findOne을 통해 saveId를 가지고 있는 회원을 찾고, findMember에 저장합니다.

(4) findMember와 member가 동일한 회원인지 확인합니다. 만일 동일한 회원이라면 join 기능이 제대로 작동한다는 것을 확인할 수 있습니다.

 

이 방법을 이전에 이야기했던 given-when-then 패턴으로 작성해보겠습니다.

 

📝 Given

  • 주어진 상황은 Member 객체 member에 "hello"라는 이름을 가진 회원이 있다는 것입니다. 따라서 member 객체를 생성하고 .setName을 통해 이름을 "hello"로 설정합니다.
Member member = new Member();
member.setName("hello");

 

🎬 When

  • 실행하는 내용은 member 객체를 join(회원 가입)하는 것입니다. join 메서드를 이용하여 member 객체를 회원 가입하고, 결과로 회원의 id를 반환받아 saveId에 저장합니다.
long saveId = memberService.join(member);

 

✅ Then

  • 나온 결과를 검증하는 것입니다. 회원 가입하고 받은 id에 해당하는 객체를 찾고, 위에서 만든 member 객체가 서로 같은지 확인합니다. MemberService에서 만들었던 메서드인 findOne을 통해 saveId에 해당하는 member 객체를 받고, assertThat을 이용해 두 객체가 같은지 확인합니다.
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());

 

위 내용의 전체 테스트 코드는 다음과 같습니다.

@Test
void join() {
    // given
    Member member = new Member();
    member.setName("hello");

    // when
    long saveId = memberService.join(member);

    // then
    Member findMember = memberService.findOne(saveId).get();
    Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}

 이 테스트를 실행해보면, 테스트가 성공적으로 통과함을 확인할 수 있습니다. 이렇게 테스트 코드를 작성하면, 코드의 기능이 제대로 작동하는지 확인할 수 있습니다.


4) 이름 중복 회원 가입 테스트 코드 작성 

회원 가입 시 중복된 이름을 가진 회원이 가입하는 것을 방지하는 기능에 대한 테스트를 작성해보겠습니다.

🔍 테스트 진행 방식

(1) Member 객체 member1과 member2를 생성하여 이름을 "spring"으로 설정합니다.

(2) member1을 join을 통해 회원 서비스에 가입시킵니다.

(3) member2를 join을 통해 회원 서비스에 가입시키는데, 이 때 IllegalStateException이 발생해야 합니다. 이 예외가 발생하면 "이미 존재하는 회원입니다."라는 메시지를 반환해야 합니다.

 

이를 두 가지 방법으로 테스트해보겠습니다.

첫 번째 방법은 try-catch를 이용한 방법이고, 두 번째 방법은 assertThrows를 이용한 방법입니다.

 

📝 Given

  • 주어진 상황은 Member 객체 member1member2가 있고, 두 회원의 이름이 모두 "spring"인 것입니다. 따라서 두 Member 객체를 생성하고 .setName을 통해 이름을 "spring"으로 설정합니다.
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");

 

🎬 When

  • 실행하는 내용은 member1member2 객체를 join(회원 가입)하는 것입니다. join 메서드를 이용하여 member1 객체를 회원 가입하고, 동일한 이름을 가진 member2를 회원 가입하려고 합니다.
memberService.join(member1);

 

✅ Then

  • 나온 결과를 검증하는 것입니다. member2를 회원 가입하려고 할 때, IllegalStateException이 발생하고, "이미 존재하는 회원입니다."라는 메시지가 반환되는지 확인합니다.

(1) 먼저 try-catch를 이용하여 테스트 코드를 작성해봅니다.

try {
    memberService.join(member2);
    fail();
} catch (IllegalStateException e) {
    Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}


(2) 다음으로 assertThrows를 이용하여 테스트 코드를 작성해봅니다.

IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

 

두 가지 방식으로 작성한 테스트 코드를 실행하면, 모두 테스트가 성공적으로 통과함을 확인할 수 있습니다. 이렇게 테스트 코드를 작성하면, 코드의 기능이 제대로 작동하는지 확인할 수 있습니다.


5) 테스트 종료 후 Repository 비우기

테스트 코드는 순서에 의존하지 않고 독립적으로 실행되어야 합니다. 그러나 만약 테스트 과정에서 DB(Repository)가 비워지지 않는다면, 테스트 코드의 순서에 따라 결과가 달라질 수 있습니다.

 

 이 문제를 해결하기 위해 각 테스트 메서드가 종료될 때마다 Repository를 비워주는 작업이 필요합니다. 이를 위해 @AfterEach 어노테이션을 사용합니다. @AfterEach 어노테이션은 JUnit5에서 제공하는 어노테이션으로, 각 테스트 메서드 실행 후에 항상 실행되는 메서드를 정의합니다.

 

이번 MemberService 테스트에서는 MemoryMemberRepository 객체를 선언하고, @AfterEach 어노테이션과 MemberRepository의 clearStore() 메서드를 이용하여 테스트 종료 후 DB를 비워주는 작업을 수행합니다.

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;
    
    @AfterEach
    public void afterEach(){
        memberRepository.clearStore();
    }
    // ...
}

위 코드에서 @AfterEach 어노테이션을 사용하여 afterEach() 메서드를 정의했습니다.

 

이 메서드는 각 테스트 메서드 실행 후에 항상 실행되며, memberRepository의 clearStore() 메서드를 호출하여 Repository(DB)를 비워줍니다.

 

이렇게 하면 각 테스트가 서로에게 영향을 미치지 않고 독립적으로 실행될 수 있습니다.


5) MemoryMemberRepository 통합하기

 테스트 코드를 작성하다보면 MemoryMemberRepository를 계속 새로 생성하는 문제가 발생할 수 있습니다. 이는 불필요한 리소스 낭비를 초래하고, 서로 다른 인스턴스를 사용하게 되어 저장소가 분리되는 문제를 야기할 수 있습니다. 이를 해결하기 위해 같은 인스턴스를 사용하도록 개선해보겠습니다.

 

※ 인스턴스 통합 방법

(1) MemberService 클래스에서 MemberRepository를 새로 생성하는 코드를 제거합니다. 이렇게 하면 MemberService 내부에서 새로운 MemoryMemberRepository 인스턴스를 생성하지 않게 됩니다.

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    // ...
}

 

(2) MemberServiceTest 클래스에서 MemberService 객체를 생성할 때, memberRepository를 매개변수로 전달해야 합니다. 이를 위해 @BeforeEach 어노테이션을 사용하여 memberRepository와 memberService 객체를 생성합니다.

class MemberServiceTest {
    MemberService memberService;
    MemoryMemberRepository memberRepository;

    @BeforeEach
    public void beforeEach(){
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }
    // ...
}