13. 회원 서비스 테스트

전에는 test 폴더의 hellospring에

repository 패키지를 직접만들어서 클래스파일을 만들었는데,

단축키를 사용하면 쉽게할 수 있다.

테스트하고자하는 클래스에서 ctrl + shift + t 누르면 바로 테스트 파일을 만들 수있다.

MemberService뒤에 Test가 자동적으로 붙는다.

밑에 체크박스 3개만 체크해서 OK누르면

자동적으로 똑같은 패키지의 폴더와 파일이 생성된다.

테스트코드는 사실 한글로 과감하게 바꿔도 된다.

프로덕션 코드가 나가는것은, (실제 테스트코드를 제외한) 한글로 이름적기가 되게 애매한데 (관례상으로도 한글로 쓰기 애매함) 뭐 우리가 영어권 사람들과 일하는게 아니면 바로바로 직관적으로 쉽게 알아들을 수 있으니까 한글도 많이 쓴다.

그리고 빌드 될때 이 테스트 코드는 포함되지 않는다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService = new MemberService();

    @Test
    void 회원가입() { 
        // 추천하는 것은 given, when, then 문법을 사용하는 것이다.
        // 테스트는 사실 이런 상황이 주어졌는데(given), 
        // 이거를 실행했을 때(when), 결과가 이게 나와야 한다!(then)
        // 이런식으로 테스트 코드가 대부분 맞춰진다.

        // 이렇게 해놓으면, 테스트가 작을때는 몰라도, 
        // 테스트가 길게되면, 만약 when을 볼때, 아~ 이걸 검증하는구나,
        // given을 보면 아~ 이 데이터를 기반으로 하는구나,
        // then을 보면 아~ 여기가 검증구간이구나 라고 `생각할 수 있다.`
        // 이렇게 주석을 적어놓고 짜는게 훨씬 도움이 많이 된다.

        // 테스트 처음할 때는 이 패턴을 권장함. 상황에 따라 이게 안맞을 순 있어서
        // 점점 더 변형해 가는걸 추천함.

        // given
        Member member = new Member();
        member.setName("hello");

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

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

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

👉result

잘된다.

근데 이거는 사실 너무 단순하다. 테스트는 정상 플로우도 중요하지만 예외 플로우가 훨씬 더 중요하다.

지금 짠 테스트는 거의 반쪽짜리 테스트나 다름없다. 회원가입의 핵심은 중복 회원 검증되는 로직을 타서 터지는 것도 봐야한다.

중복 회원 예외코드를 따로 짜보자.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService = new MemberService();

    @Test
    void 회원가입() { 

        // given
        Member member = new Member();
        member.setName("hello");

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

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

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring"); // 중복된 이름으로 가입.

        //when
        memberService.join(member1);
        try {
            memberService.join(member2);
            fail(); 
            // junit 메서드, 강제로 fail 시킴. 이 코드까지 온다는건 중복회원이 아니란 뜻이기 떄문
        } catch (IllegalStateException e) {
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
            // 이 메시지는 join 메서드에서 예외처리한 메시지임.
        }
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

👉result

잘 동작한다.

만약 이렇게 일부로 틀리게 메시지를 넣으면 에러가 뜬다.

이렇게 메시지를 틀릴 수 있기때문에 try catch 쓰기가 애매한데, 더 좋은 문법이 있음. assertThrows를 사용한다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService = new MemberService();

    @Test
    void 회원가입() { 

        // given
        Member member = new Member();
        member.setName("hello");

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

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

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring"); 

        //when
        memberService.join(member1);
        assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        // IllegalStateException이 터지기를 기대하고 있음
        // 람다식을 사용해서 member2를 넣으면 예외가 터져야 한다.

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

👉result

잘통과한다.

만약 IllegalStateException 대신 일부로 NullPointerException을 넣는다면?

 assertThrows(NullPointerException.class, () -> memberService.join(member2));

👉result

예외 타입이 안맞다고 에러가 뜬다.

그리고 메시지까지 검증하고 싶으면

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

👉result

이렇게 아예 assertThrows를 익셉션 변수 e로받고 따로 assertThat으로 검증하면 된다.

이제 모두 완성했다.

전체 클래스를 다 테스트해도 잘된다. 근데 이거는 우리가 테스트를 구현할때

이름을 다르게 썼기때문에 충돌이 안난 것이었다. 만약 저 회원가입 테스트 메서드에 hello대신 spring으로 바꿨다면?

DB에 데이터가 누적이되고 제대로 삭제되지 않아서 회원가입 테스트 메서드가 에러가 발생하게 된다.

그래서 또 clear를 해줘야한다.

근데 문제가 지금 이 MemberServiceTest에는 멤버서비스 클래스만있고 리포지토리클래스가 없다.

그래서 멤버 리포지토리를 가져와야 한다.

그리고 매번 테스트할때마다 DB를 비워준다.

👉result

잘된다.

참고로 shift + F10(윈도우 기준) 을 누르면 이전에 실행된 것이 실행됨 (아주 유용함)


여기서 이상한 점을 느낄 수 있다.

멤버서비스 테스트와 멤버서비스에서 사용하는 리포지토리 객체가 new로 인해 서로다른 인스턴스를 생성하는 것이 애매하고 찝찝하다..

물론 지금 static으로 선언 되어있기 때문에 문제가 없지만 만약

private Map<Long, Member> stroe = new HashMap<>();

static을 없애면 바로 문제가 생긴다. 완전히 다른 db가 되기 때문이다.

지금 결국 어쨋든간에 같은 리포지토리로 테스트해야하는데, 다른 리포지토리로 테스트한거기 때문에 같은 인스턴스를 쓰도록 바꾸려면

생성자를 따로만들어 준뒤

BeforeEach 메서드와 앞에서 만든 생성자를 통해 테스트 실행할때마다 같은 메모리의 리포지토리 인스턴스를 사용하게 만들 수 있다.

멤버 서비스 입장에서 내가 리포지토리를 만들어주는게 아니라 외부에서 넣어준다.

이런거를 Dependency Injection(DI) 이라고 한다.

최종 코드

MemberService.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) { //! MemberRepository를 직접 new 해서 넣는게 아니라
                                                              //! 외부에서 넣어주도록 생성자를 만든다.
        this.memberRepository = memberRepository;
    }

    /**
     * 회원 가입
     */
    public Long join(Member member) {

        validateDuplicateMember(member); // 중복 회원 검증
        memberRepository.save(member);
        return member.getId();

    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(M -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    /**
     * 전체 회원 조회
     */
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    /**
     * 한 회원 조회
     */
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

MemberServiceTest.java

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService memberService; //! 여기다 바로 new 하지 않는다.
    MemoryMemberRepository memberRepository; //! 여기다 바로 new 하지 않는다.

    @BeforeEach //! 테스트 메서드 실행 전에 항상 실행됨.
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository(); //! 여기서 리포지토리 인스턴스를 만들고
        memberService = new MemberService(memberRepository); //! 생성자를 통해 멤버서비스의 리포지토리 또한 같은 인스턴스로
                                                             //! 만들어 준다.
    }

    @AfterEach
    public void afterEach() {
        memberRepository.clearStore(); //! 테스트할때마다 DB 비워준다.
    }

    @Test
    void 회원가입() { 

        // given
        Member member = new Member();
        member.setName("spring");

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

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

    @Test
    public void 중복_회원_예외() {
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when
        memberService.join(member1);

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

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

출처 : 인프런의 김영한 선생님 강의를 정리한 글입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8/dashboard

댓글

Designed by JB FACTORY