[Spring Boot] Test를 통한 검증
우리가 다른 프레임워크를 포함해서 백엔드를 개발하고 API를 개발할 때는 확실히 작동하는 코드인지 검증이 되어야한다. 코드를 작성하고 직접 실행해서 잘 작동하는지 확인하는 방법도 있지만, 이는 시간적 손해가 크다. 이를 보완하기 위한 Test 코드 작성법이 있다.
Member.java
public class Member {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
MemberRepository.java
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
MemoryMemberRepository.java
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
public void clearStore() {
store.clear();
}
}
이러한 도메인과 레포지토리가 있다고 생각해보자. 이 로직이 제대로 작동하는지 확인하는 방법으로는 직접 실행을 해서 View에서 직접 값을 넣고 하는 방법이 있을 수 있다. 하지만 이런 방법보다는 Test를 통한 방법이 더 시간적으로 절약할 수 있다.
Spring Boot 프로젝트를 생성하고 나면 main과 test가 둘다 생성된다. main에서 패키지를 만들고 자바 파일을 작성한 것과 동일한 방법으로 test에서도 동일한 패키지와 자바파일이름에 test만 붙인 형태로 생성하고 test 케이스를 작성하는 것이 관례라고 생각하며 된다.
MemoryMemberRepositoryTest.java
import static org.assertj.core.api.Assertions.*;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach(){
repository.clearStore();
}
@Test
public void save(){
Member member = new Member();
member.setName("Spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
// 1.
System.out.println(result == member);
// 2.
Assertions.assertEquals(member, result); // 기대되는 것, 찾은것
// 3.
assertThat(member).isEqualTo(result); // 문장 그대로 읽힌다
}
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring2").get();
assertThat(result).isEqualTo(result);
}
@Test
public void findAll(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
위에 주석 1. 2. 3. 처럼 테스트를 확인할 수 있는 3가지 방법이 있다.
첫번째는 System.out.println(result == member); - 그냥 print로 찍어보는 방법이다. 근데 이 방법은 좀 원시적인 방법이다.
두번째는 Assertions.assertEquals(member, result); - junit.jupiter의 Assertions를 이용해서 확인하는 방법이다. member는 기대하는 값이고 두번재 인자는 결과이다. 이 두개가 동일하면 테스트 케이스 통과인 것이다.
세번째는 assertThat(member).isEqualTo(result); - assertions.jcore의 Assertions를 이용하는 것이다. 여기서 Assertions가 보이지 않는데, 이는 static으로 빼놨기 때문에 바로 호출이 가능하게 해두었기 때문이다.
세번째가 가장 이상적인데 essertThat(member).isEqualTo(result) 이것을 그대로 읽어봤을때 영어를 조금 읽는다면 그대로 읽힌다.
이런식으로 테스트 코드를 작성해서 각각의 메서드를 테스트할 수도 있고 클래스 전체를 테스트할 수도 있다.
문제는 클래스 전체를 테스트하는데에 있다. 같은 메모리를 활용하고 있기 때문에 각각의 케이스에서 멤버 중복이 일어날수 있다는 점이다. 이렇게되면 당연히 케이스를 통과하지 못하는 경우가 생긴다.
이를 방지하기 위해서는 @AfterEach 어노테이션을 통해 각각의 케이스마다 저장소를 클리어 해주는 로직이 포함되어 있어야한다. 그래야 클래스 전체를 테스트 했을때 각각의 테스트 결과를 올바르게 확인할 수 있다.
이번에는 서비스 로직에 대한 테스트를 살펴보자
MemberService.java
public class MemberService {
private MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 회원가입
public Long join(Member member) {
// 같은 이름이 있는 중복 회원은 안된다.
// 1.
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(member1 -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
// 2.
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
// 전체회원조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
// id로 찾기
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(member1 -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
}
여기서 join 메서드를 보자. 회원가입을 할 때 같은 이름은 중복이 불가능하다고 가정하자. 이때는 로직으로 같은 이름의 사람이 가입되면 예외를 발생시켜야한다.
첫번째 방법은 Optional<Member> result = memberRepository.findByName(member.getName()); Optional result에 넣고 그에 대해서 ifPresent, 즉 member가 존재하면, 예외를 발생시키는 것이다.
이렇게 할 수도 있고 두번재 방법으로는 메서드로 빼는 방법이 있다. 메서드로 추출하고 싶은 코드 블록을 ctrl + alt + m을 누르면
이렇게 창이 뜬다. 원하는 방식으로 선택하고 저장하면 메서드로 추출된다.
이제 테스트코드를 보자
MemberServiceTest.java
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository); // 이런걸 DI라고함 의존성 주입
}
@AfterEach
public void atferEach(){
memberRepository.clearStore();
}
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("hello");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(findMember.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);
// 1.
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
// 2.
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
여기서 중복_회원_예외 메서드를 보자. 현재 member1 2가 같은 이름으로 저장되고 있다. 여기서 중요한 점은 중복되고 있다는 점이다. 더 중요한 점은 중복이 안되었을 때 통과를 하는 것도 중요하지만, 중복되었을 때는 어떻게 테스트를 통과시킬 것이냐 라는 점이다.
먼저 첫번째 방법은 memberService를 아예 try catch로 감싸는 것이다. (try catch는 ctrl + alt + t를 누르면 빠르게 감쌀 수 있다.) 감싸고 e 의 메시지가 중복된다는 MemberService에서의 문구와 같으므로 중복된다는 테스트 케이스에서도 통과된다.
두번째 방법으로는 e를 assertThrows를 통해서 받아온 후, 문구를 비교하는 것이다. 터진 예외가 IllegalStateException.class일 경우에 e를 담아서 아래의 assertThat을 통해서 e 메시지를 비교해서 테스트를 하고 검증한다.
이런식으로 테스트를 진행한다. 이런 방식은 직접 코드를 만들고 그 코드가 잘 작동하는지 확인하는 테스트라고 보면 될것 같다. 이를 역으로 이용해서 테스트를 먼저 작성하고 그 코드를 기반으로 실제 구현을 하는 방법으로 TDD라는 방법이 있다. 이에 대해서는 나중에 알아보는 것으로 하자.