이 글은 인프런에 있는 김영한님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술" 강의를 듣고 정리한 필기입니다.
⛅️ 회원 서비스(MemberService) 테스트 클래스 만들기
이전에 Repository 기능 테스트할 때에 test package에 직접 repository package와 class를 생성했다.
하지만 직접 생성하지 않고도 생성할 수 있는 방법이 있다.
1. test package와 class를 만들고 싶은 클래스 명을 누르고 Alt+Enter키 - Create Test 선택
2. 밑과 같은 창이 뜨는데, test를 하고 싶은 Member를 선택하고, OK를 누른다.
그러면 아래와 같이 package와 class가 생성되었음을 확인할 수 있다. 멤버로 선택한 method에 @Test 어노테이션 또한 붙어 있다.
이렇게 만들어진 클래스에 Test 코드를 하나씩 채우면 될 것이다.
이 다음에는 MemberService의 메서드들을 테스트할 것이다.
하지만 회원 서비스(MemberService)의 메서드들을 테스트하기 위해서는 회원 서비스 객체가 있어야 할 것이다.
그러므로 MemberService 객체를 생성한다.
class MemberServiceTest {
MemberService memberService;
}
⛅️ given, when, then 패턴
given-when-then 패턴은 test code 작성 시 자주 사용하는 패턴이다.
테스트 코드의 구조를 생각해보면, 어떤 상황이 주어져서(given), 이것을 실행했을 때(when), 어떤 결과가 나와야 한다(then)이라는 구조를 가지고 있음을 알 수 있다.
테스트 코드 작성 시, 준비 - 실행 - 검증 세 부분으로 나누어서 작성하면, 테스트 코드가 길어졌을 때 각 부분(given, when, then)을 보면 테스트 코드를 이해하는데에 도움이 된다.
Given
테스트를 위해 준비를 하는 과정이다.
테스트에서 사용되는 변수, 입력 값들을 정의하거나, Mock 객체를 정의하는 부분이 이에 해당된다.
When
실제로 action을 하는 테스트를 실행하는 과정이다.
하나의 메서드만 수행하는 것이 바람직하다. When은 대체로 가장 중요하지만 가장 짧다.
Then
When에서 실행한 결과를 검증하는 과정이다. 예상 값(expected)와 실제 값(actual)을 비교한다.
주로 assertThat 구문을 사용하여 검증한다.
⛅️ 회원 가입(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에 hello라는 이름을 가진 회원이 있다는 것이 지금 주어진 상황이다.
따라서 member 객체를 생성하고 .setName을 통해 이름을 hello로 설정한다.
when은 실행하는 내용이다.
간단하다. member 객체를 join(회원 가입)하는 것이다.
join 메서드를 이용하여 member 객체를 회원 가입하고, 결과로 회원의 id를 반환받아 saveId에 저장한다.
then은 나온 결과를 검증하는 것이다.
회원 가입하고 받은 id에 해당하는 객체를 찾고, 위에서 만든 member 객체가 서로 같은지 확인하면 된다.
MemberService에서 만들었던 메서드인 findOne을 통해 saveId에 해당하는 member객체를 받고,
assertThat을 이용해 두 객체가 같은지 확인하면 된다.
@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());
}
그리고 이 메서드를 테스트 돌려보면 테스트를 통과함을 알 수 있다.
⛅️ 이름 중복 회원 가입 테스트 코드 (try-catch문, assertThrows)
위 테스트를 통해 join이 작동하는 것을 확인할 수 있지만, 모든 것을 확인해 본 것이 아니다.
join에서 제일 중요한 부분을 빼먹었다!
join 메서드에서 회원 가입이 잘 되는 것도 중요하지만, 더 중요한 것은 중복 회원 가입을 막는 것이다.
새로운 test 메서드를 작성하는데, 이름이 같은 두 회원을 회원 가입 시켜서 예외가 발생하도록 해보자.
memberDuplicateException이라는 이름의 메서드를 하나 작성하고, 여기에서도 given-when-then을 적용해보자.
주어진 상황(given)은 두 개의 회원 객체가 있고, 두 회원 모두 이름이 spring인 것이다.
실행할 내용(when)은 두 회원을 회원 가입(join)하는 것이다.
다음엔 결과를 검증(then)해야 한다.
만일 예외가 제대로 터졌다면 MemberService의 validateDuplicateMember 메서드에서 예외 메세지로 설정한 "이미 존재하는 회원입니다."가 나와야 할 것이다. 따라서 이 메세지가 제대로 나왔는지 확인하자.
발생한 예외를 두 가지 방법으로 확인해 볼 것이다. 첫 번째는 try-catch 문이고, 두 번째는 assertThrows문이다.
먼저 try-catch문을 이용해보자.
try-catch는 밑과 같은 모양을 띄고 있다. try 블럭에서는 예외를 발생시킬 수 있는 코드를 넣고, catch문에는 exception이 발생했을 때 예외를 처리하기 위한 코드를 작성하면 된다.
try{
}catch(Exception exception){
}
@Test
public void memberDuplicateException(){
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
//then
try{
memberService.join(member2);
fail();
}catch (IllegalStateException e){
// assertThat을 통해 두 객체가 같은지 확인한다.
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
두 번째로 assertThrow를 사용해보자.
assertThrow는 junit에 있는 메서드로써, assertThrows(Class expectedType, Executable executable)의 기본 형태를 가지고 있다.
executable block이나 lambda식을 실행했을 때, expectedType의 예외가 발생했는지 확인한다.
예외가 발생했다면, 예외 객체를 반환하는데, 반환된 객체를 이용하여 여러가지 확인을 할 수 있다.
우리는 join에서 만일 중복 회원이 발생한다면 IllegalStateExcepion을 발생시키고, "이미 존재하는 회원입니다" 메세지를 내보내도록 했다.
테스트에서 중복 회원을 발생시켰을 때 위와 같이 작동하는지 확인하면 된다.
@Test
public void memberDuplicateException(){
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
//then
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
then 부분이 바뀐 것을 알 수 있다.
() ->memberService.join(member2)라는 Lambda식을 실행했을 때, IllegalStateException이 발생한다면 PASS할 것이다.
그리고 IllegalStateException 변수 e에 저장한다.
assertThat을 통해 e에 저장된 메세지가 "이미 존재하는 회원입니다."와 일치하는지 확인한다.
이 테스트를 실행했을 때 통과가 된다면 join의 기능이 정상 작동한다는 것을 알 수 있다.
⛅️ 테스트가 끝날 때마다 Repository를 비워주기
repository 테스트 때에도 서로 다른 테스트인데 DB(Repository)를 비워주지 않아 테스트에 문제가 생길 때가 있었다.
이렇게 되면 테스트 코드가 순서에 의존하게 된다는 문제점이 있었다.
테스트 코드는 순서에 의존하게 되면 안되기 때문에, 이를 해결하기 위해 각 테스트 메서드가 끝날 때마다 repository를 비워야 한다.
이번 MemberService 테스트도 마찬가지로 repository를 비우는 일을 해야한다.
Repository에 접근해야하기 때문에, MemoryMemberRepository 객체를 선언한다.
그리고 @AfterEach 어노테이션과 MemberRepository의 메서드인 clearStore()를 이용하여 DB를 비워준다.
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memoryMemberRepository;
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
...
}
⛅️ MemoryMemberRepository 통합하기
테스트 코드를 작성하다보니 걸리는 부분이 하나 생겼다.
test class마다 new를 통해 MemoryMemberRepository를 계속 새로 생성하고 있다는 점이다.
MemberService에서 new를 통해 MemoryMemberRepository를 새로 생성하고 있기 때문에, 여기서 생성한 repository 객체가 MemoryMemberRepository와 같은 객체라는 보장이 없다는 뜻이다.
현재는 MemoryMemberRepository에 static으로 되어 있지만, 만일 static이 아니라면 서로 다른 인스턴스이기 때문에 저장소가 달라지게 될 것이다.
같은 인스턴스를 사용하도록 바꿔보자.
1. 우선 MemberService 클래스에서 MemberRepository를 new를 통해 생성했던 코드를 제거한다.
2. memberRepository를 클릭 - Alt+Enter 누르기 - Add Constructor Parameter를 선택한다.
그러면 생성자에서 memberRepository를 매개변수로 받아서 변수에 넣어준다.
이렇게 하면 new를 통해 객체를 하나 생성하는 것이 아니라, 외부에서 객체를 받아서 넣어주는 방식으로 바뀐다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
3. MemserServiceTest 클래스에서 MemberService 객체를 생성할 때, memoryRepository를 매개변수로 전달해야 한다.
그리고 테스트 함수는 독립적으로 실행되어야 하기 때문에 @BeforeEach를 이용해서 memberRepository와 memberService객체를 생성하자.
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
...
}
MemberService 입장에서는 new를 이용해 인스턴스를 생성하지 않는다. 매개변수를 통해 외부에서 memberRepository를 전달해준다.
MemberService 입장에서는 이것을 Dependence Injection, DI라고 한다.
다음 시간에서 DI에 관련된 것들을 조금 더 알아보도록 하자.