이전에 테스트할 때는 직접 package를 만들고 클래스를 만들고 하였었는데,
훨씬 편하게 생성하는 단축키가 존재한다.
**단축키: command + shift + t
이 단축키를 사용하여 MemberServiceTest class를 자동 생성 하였다.
test - java - hellospring - service 밑에 MemberServiceTest 클래스가 자동 생성 되었다. 게다가 틀까지 모두 짜준다(ㄱㅇㄷ).
**참고) Test는 @Test void 회원가입() { } 처럼 한글로 이름을 작성해도 된다.
그리고 이제 내용을 채워넣을 차례이다.
먼저 MemberService memberService = new MemberService();를 만들었다.
회원가입을 하려면 먼저 member부터 만들어야 하는데,
이 때 추천하는 문법이 있는데
given - when - then 문법이다.
테스트 코드는 나누자면
무언가가 주어졌는데, 이를 실행했을 때, 무언가가 나와야 한다.
이 세 가지 단계로 구성된다.
테스트를 이 세 가지 단계로 나누어서 작성하면 테스트 코드가 길 때도
given을 보고 '아 이 데이터를 기반으로 하는구나'
when을 보고 '아 이걸 검증하는 거구나'
then을 보고 '여기가 검증부구나'를 알 수가 있다.
이 주석들을 깔고 코드를 짜면 훨씬 편하다.
일단 //given부분에
Member member = new Member();
member.setName("hello");
를 작성하고,
//when부분에 (검증할 내용 작성)
Long saveId = memberService.join(member);
를 작성하였다.
memberService의 join을 검증하는 것이기 때문에
memberService.join(member); 를 작성하고,
return은 저장한 Id가 나와야하므로
Long saveId = memberService.join(member);
로 코드를 수정한 것이다.
//then부분에는 (검증)
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
를 작성하였다.
우선 내가 저장한 것이 repository에 있는 게 맞는지를 찾아보고 싶기 때문에 repository를 꺼내야 한다.
memberService.findOne(saveId);
saveId를 memberId로 넘기는 것이다.
return값이 Optional이기 때문에
Optional one = memberService.findOne(saveId);
로 코드를 수정해주고,
단순화하기 위해 바로 get()으로 받도록 코드를 수정해준다.
그래서 최종적으로
Member findMember = memberService.findOne(saveId).get();
이 된 것이다.
그리고 나서 member의 이름이 findMember의 이름과 같은지를 검증하기 위해서
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
위 코드를 작성하였는데,
단축키: option + enter을 눌러 static import로 대체해주었다.
그래서 최종적으로
assertThat(member.getName()).isEqualTo(findMember.getName());
이 된 것이다.
실행했을 때 모두 잘 돌아가는 것이 확인되었다.
Test는 정상 플로우도 중요한데, 예외 플로우가 훨씬 더 중요하다.
join의 핵심은 저장이 되는 것도 중요하지만, 중복 회원 검증 logic을 잘 타서 예외가 잘 터뜨려지는가 를 확인하는 것도 중요하다.
이를 확인하기 위해서 중복 회원 예외가 실제로 발생하는 test code도 작성해 보았다.
@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문법을 이용해서 작성하였다.
given 부분:
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
member1에 spring이라는 이름을 저장하였고, member2에도 spring이라는 이름을 저장하였다.
when 부분:
memberService.join(member1); //여기까진 문제 없음
try {
memberService.join(member2);
//두 번째 join부터 validateDuplicateMember에서 걸려서
예외가 터져야 하기 때문에 예외를 잡기 위해 try-catch문 안에 작성하였다.
fail();
//memberService.join(member2)줄에서 예외가 터지지 않고
그 다음줄로 내려가면 실패인 것이기 때문에 fail()이라고 적었다.
}catch (IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//예외 메시지가 MemberService 클래스의
validateDuplicateMember에 작성한 에러 메시지와 동일한지
확인하는 코드를 작성하였다.
}
성공! 잘 돌아간다.
이 경우에서 try catch문을 사용하는 것보다 더 좋은 문법이 존재한다.
assertThrows라는 문법이다.
try catch문을 모두 주석처리 하고, asserThrows를 사용한 다음 코드를 작성하였다.
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
memberService.join(member2)를 실행할 때, IllegalStateException.class 예외가 터져야 한다는 뜻이다.
그럼 메시지는 어떻게 검증할까?
아까 작성했던
assertThrows(IllegalStateException.class, ()
->memberService.join(member2)); 코드에 단축키 command + option + v를 사용하여
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));로 코드를 수정하였다.
그 후
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); 라는 메시지 검증 코드를 작성하였다.
회원가입의 setName을 spring으로 바꾸어봤더니, 제대로 돌아가지 않았다.
그 이유는 코드를 돌릴 때마다 join에서 database에 memory가 계속 쌓여서, name의 값이 spring인
값이 계속 누적이 되었기 때문이다.
spring은 이미 가입이 되어있는 것이다.
그래서 여기서도 clear을 시켜줘야 한다.
기존에는 MemberService memberService = new MemberService(); 밖에 안 되어 있었기 때문에
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
를 추가하고, MemoryMemberRepositoryTest에 작성했던
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
AfterEach를 가져와서 새로 작성하였다.
--> 실행될 때마다 DB의 값을 clear해준다.
아까는 hello를 spring으로 바꿨을 때 제대로 동작하지 않았었는데, clear을 해준 지금은 제대로 동작하는 것을 볼 수 있다.
**참고) 단축키: control + r -> 이전에 실행했던 것을 재실행 해줌
그런데 여기서 문제가 하나 있다.
현재 MemberService 클래스에 있는
private final MemberRepository memberRepository = new MemoryMemberRepository();
코드의 MemoryMemberRepository와,
MemberServiceTest 클래스에 있는
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
코드의 MemoryMemberRepository가
서로 다른 객체라는 것이다.
물론 현재는 MemoryMemberRepository 클래스의
private static Map<Long, Member> store = new HashMap<>(); 코드를
static으로 선언을 해놓았기 때문에 문제가 발생하지 않았지만,
static이 아니었다면 분명 문제가 발생했을 것이다.
이 뿐만이 아니라 그냥 원래 MemberService 클래스와 MemberServiceTest는 같은 Repository를 테스트 하는 것이 맞는 것이다.
그래서 해결 방법은, 둘이 같은 instance를 쓰게 만들어야 한다.
우선 MemberService 클래스에서
private final MemberRepository memberRepository = new MemoryMemberRepository();
를
private final MemberRepository memberRepository;
로 바꾸고,
constructor를 사용하여 memberRepository를 직접 new에서 생성하는 것이 아니라, 외부에서 생성하도록 바꿔준다.
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
그리고 MemberServiceTest 클래스에 가서,
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
였던 코드에서,
MemberService를 생성할 때 MemoryMemberRepository를 직접 넣어주는 코드로 수정해준다.
@BeforeEach로, 동작하기 전에 넣어주는 코드를 작성한다.
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
beforeEach이므로, 각 테스트를 실행하기 전에
memberRepository = new MemoryMemberRepository();
MemoryMemberRepository를 만들고, 그것을 memberRepository에 넣는다.
memberService = new MemberService(memberRepository);
그리고 MemberSevice를 만들어서 memberRepository를 넣으면, 같은 repository가 되는 것이다.
memberService입장에서는 직접 new하지 않고 외부에서 넣어주는 것인데, 이런 것을
Dependency Injection이라고 한다.(DI)