Spring boot기반 Web Application 개발[11] - 회원 서비스 테스트(Junit)

4 minute read

이전 포스팅에서 구현했던 service를 테스트해보자. 마찬가지로 자바 테스트 프레임워크 Junuit을 활용한다.

테스트 클래스 자동 생성

이번에는 단축키를 이용하여 테스트 클래스를 만들어보자. 우선, 테스트가 필요한 service 클래인 MemberService.java로 가보자. 그리고 단축키 ctrl + shift + t를 클릭하고, Create new Test 버튼을 클릭하자.

스프링부트 태스트 클래스 생성

보는 것처럼 class name을 자동으로 설정해준다. 아래 Member 태그아래 테스트하고자 하는 메소드 check box를 클릭하자.

image

MemberSerivceTest.java가 자동 생성된 것을 확인할 수 있다.

junit test

Join Test

이제 테스트코드를 작성해보자. join 기능부터 시작한다. 아래 코드를 MemberSerivceTest.java에 작성해주자. 기본적인 join 기능은 회원 리포지터리 테스트에서 정상동작 함을 확인했을 것이다. 이번에 비즈니스 로직 테스트에 중점을 두고, 중복_회원_예외 메서드를 작성해보자.

try - catch

파일명 : MemberSerivceTest.java
위치 : \test\java\hello.hellospring\service\MemberSerivceTest.java
   MemberService memberService=new MemberService();

    @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);
        try {
            memberService.join(member2);
            fail();
        }catch (IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
        //then
    }
  • 테스트코드에서는 메서드 이름을 한글로 작성해도 문제없다.
  • [given - when - then] 형식으로 코드를 작성해보자.
  • 두 객체를 생성하고, 같은 이름을 넣어보자
    • 이때, 실패가 나와야 옳은 결과이다.
    • try - catch 문을 활용해 실패했을 때, 나오는 메시지가 일치하는지 확인해보자.

junit 테스트 성공

정상적인 결과가 나온다.

assertThrows

try - catch 문을 더 간결하게 표현할 수 있다.

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

try - catch 문을 아래와 같이 변경해보자. 두 코드는 동일하게 동작한다.

assertThrows(IllegalStateException.class,()-> memberService.join(member2));
  • assertThrows(오류명.class(), 실행 메소드) 형식으로 지정할 수 있다.
파일명 : MemberSerivceTest.java
위치 : \test\java\hello.hellospring\service\MemberSerivceTest.java
package hello.hellospring.service;

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

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

class MemberServiceTest {

    MemberService memberService=new MemberService();

    @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);
        assertThrows(IllegalStateException.class,()-> memberService.join(member2));
		assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

junit 테스트 성공

정상동작한다.

테스트 코드가 정상동작하는지 확인하기 위해서는 그 반대 기능도 테스트해봐야한다. IllegalStateException 에러 구문을 가장 익숙한 NullPointerException으로 바꿔보자.

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

junit 테스트 실패

fail이 나온다. MemberService.java에서 던진 에러 IllegalStateException와 다르기 때문이다.따라서, 테스트 코드는 정상적으로 동작한다고 할 수 있다.

getMessage()

메시지도 테스트해볼 수 있다. 코드를 살짝 변경해보자.

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

이 코드를 아래와 같이 바꿔보자.

IllegalStateException e = assertThrows(IllegalStateException.class, 
() -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
  • throw 에러에서 추출 된 메시지가 일치하는지 테스트
  • alt + enter를 입력하고, split into declarations and assignment를 클릭하면 자동으로 지역변수(위 코드에서 IllegalStateException e)가 생성된다.

junit 테스트 성공

message 테스트도 정상적으로 실행된다.

통합 테스트

테스트는 항상 테스트 코드가 실행된 후, 다시 예전 상태로 돌려놔야한다. 그래야만 다른 테스트가 정상적으로 동작하는지 판단할 수 있다. 예를들어, TestA 테스트 메소드 실행 시 DB에 A라는 객체를 삽입했다면, TestB메소드를 실행되기 전에 A라는 객체를 다시 삭제해줘야한다. 테스트전의 상태로 되돌리는 것이다.

위에서 작성한 MemberSerivceTest.java 에서 통합 테스트(Class 테스트)를 실행하면 오류가 발생할 것이다. 위와 같은 문제 때문이다. 이와 관련하여 실습해보자. 아래코드를 MemberSerivceTest.java에 추가하고, 테스트를 실행해보자.

MemoryMemberRepository memoryMemberRepository= new MemoryMemberRepository();

@AfterEach
public void afterEach(){
	memoryMemberRepository.clearStore();
}
  • memoryMemberRepository 인스턴스는 가상의 dbrepository이다.
  • @AfterEach 어노테이션을 추가하면, 매 테스트 코드가 실행된 후에 afterEach() 메소드가 실행된다.
    • db를 비워주는 작업과 동일하다고 생각하면 좋다.

junit 전체 테스트 성공

정상동작한다.

생성자 - 의존성 주입(DI)

하지만, 위 코드도 한 가지 문제점이 있다. new 키워드를 통해 Test class 내에 repository를 생성하고 있다. 다시말해, MemberService.java에 있는 memberRepository 인스턴스와 MemberServiceTest.java에 있는 memberRepository 인스턴스가 서로 다르다. 다른 저장소로 작업을 하고 있는 것이다. 동일한 저장소에 접근하도록 변경해보자.

우선, MemberService.java 에서 new 생성자를 삭제해주고 아래 코드를 추가하자.

public MemberService(MemoryMemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
  • 생성자를 통해 인스턴스의 저장소를 할당해준다.
파일명 : MemberService.java
위치 : main\java\hello.hellospring\service\MemberService.java
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.respository.MemoryMemberRepository;

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


public class MemberService {

    private final MemoryMemberRepository memberRepository;

    public MemberService(MemoryMemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }


    /**
     * 중복 회원 관리
     */
    public Long join(Member member){
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

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

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

    /**
     * 회원 아디로 조회
     */
    public Optional<Member> findOne(Long memeberId){
        return memberRepository.findID(memeberId);
    }

}

다음으로 MemberServiceTest.javanew 생성자를 삭제하고, 아래 코드를 삽입하자.

@BeforeEach
public void beforeEach(){
	memoryMemberRepository=new MemoryMemberRepository();
	memberService=new MemberService(memoryMemberRepository);
}
  • @BeforeEach 어노테이션은 @AfterEach와 반대이다. 테스트 메소드를 실행하기전에 무조건 실행된다.
  • MemberService.java 클래스와 MemberServiceTest.java 클래스는 이제 동일한 repository를 갖게 된다.
  • 테스트가 독립 실행되는 것을 보장할 수 있다.
파일명 : MemberServiceTest.java
위치 : \test\java\hello.hellospring\service\MemberSerivceTest.java
package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.respository.MemoryMemberRepository;
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;
    MemoryMemberRepository memoryMemberRepository;

    @BeforeEach
    public void beforeEach(){
        memoryMemberRepository=new MemoryMemberRepository();
        memberService=new MemberService(memoryMemberRepository);
    }

    @AfterEach
    public void afterEach(){
        memoryMemberRepository.clearStore();
    }

    @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() {
    }
}

지금 이 상황은 MemberService.java 의 생성자가 외부 동작(Test 코드의 BeforeEach)에 의해 프로퍼티가 결정된다고 얘기할 수 있다. 이것이 바로 의존성 주입(DI)이다. 말그대로 외부에서 의존성을 주입해주는 것이다.

의존성 주입(DI) 과 관련 된 자세한 내용은 다음 포스팅에서 다뤄봐야겠다.


이 포스팅은 인프런 김영한님의 스프링 입문 - 코드로 배우는 스프링 부트 강의를 토대로 작성되었습니다.

Reference

Leave a comment