Test case 작성
스프링 부트에서는 테스트 메서드 작성 시 메서드 앞에 @Test 어노테이션을 명시한다.
각각의 테스트 메서드는 독립적으로 실행되어야 한다.
테스트는 각 테스트끼리의 순서가 관계없어야 하고, 의존 관계가 없어야 한다. 따라서 하나의 테스트가 끝날 때마다 공용 데이터들을 깔끔하게 지워야 한다. 이를 위해 @BeforeEach와 @AfterEach 어노테이션을 사용한다.
테스트 메서드 작성 시 메서드 명은 한글로 작성해도 상관없다. 빌드 시 실제 코드에 포함되지 않기 때문이다.
@SpringBootTest
스프링 부트를 사용해 Test 한다는 것을 명시한다. 데이터베이스 정보를 스프링에서 가지고 있을 때 사용한다.
해당 어노테이션이 붙으면 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional
테스트 케이스에 어노테이션을 붙이는 경우 테스트 시작 전 트랜잭션을 시작하고, 테스트 완료 후 롤백한다. 따라서 데이터베이스에 테스트 때 변경한 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
@AfterEach
@AfterEach 어노테이션이 명시된 메서드는 테스트 메서드 실행 후에 무조건 실행된다.
아래는 @AfterEach를 작성하지 않은 테스트 코드이다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
// Assertions을 static으로 선언하여 assertThat() 메서드를 바로 사용한다.
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1"); // spring1 회원 가입
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2"); // spring2 회원 가입
repository.save(member2);
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("spring1"); // spring1 회원 가입
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2"); // spring2 회원 가입
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
findByName() 메서드에서는 테스트가 올바르게 진행될 시 assertThat() 메서드를 사용해 spring1이라는 이름을 가진 사용자 객체 주소와 member1 객체 주소 값을 비교하고, 두 주소 값이 같아야 한다.
각각의 테스트 메서드를 별개로 실행하면 테스트 실패 없이 실행된다.
그러나 클래스 전체 테스트 실행 시 findByName() 테스트 메서드 부분에서 실패가 발생한다.
클래스 전체 테스트 시, 테스트 메서드가 작성된 순서대로 실행되지 않기 때문이다.
findByName() 메서드 실행 전 findAll() 메서드가 먼저 실행되어 spring1이라는 이름을 가진 member1 객체가 이미 생성되었다. 따라서 findByName()에서 만든 member1와는 다른 주소 값을 가지게 되므로 테스트 오류가 발생한다.
해당 실패를 해결하기 위해서는 테스트 메서드를 실행하고 나서 저장한 데이터를 clear 해주어야 한다.
@AfterEach
public void afterEach() {
// 실행 내용
}
@AfterEach 어노테이션을 붙인 메서드는 테스트 케이스가 실행되고 나서 무조건 실행된다.
위에서 작성한 테스트 코드를 예시로 들었을 때 save() 메서드가 끝나고 afterEach() 메서드가 실행되고, findByName() 메서드가 끝나고 afterEach() 메서드가 실행된다.
afterEach() 메서드를 작성하기 전, MemoryMemberRepository.java에서 clearStore() 메서드를 작성해준다.
이때 store 변수는 HashMap 타입으로, 회원 정보(id, name)를 담는다.
public void clearStore() {
store.clear();
}
clear()를 실행하면 Map에 담긴 데이터를 전부 삭제하고 size를 0으로 만든다.
테스트 코드에서 clearStore() 메서드를 사용한다.
@AfterEach
public void afterEach() {
repository.clearStore();
}
테스트 코드에 위 afterEach() 메서드를 작성해주면 클래스 전체를 테스트했을 때도 실패 없이 모두 성공으로 실행된다.
테스트 메서드가 실행되고 난 뒤 afterEach() 메서드가 실행되어 store에 저장되어 있던 내용을 삭제하기 때문이다.
@BeforeEach
@BeforeEach 어노테이션을 명시한 메서드는 테스트 메서드 실행 전에 무조건 실행된다.
기존 MemberService 클래스에서는 아래와 같이 MemoryMemberRepository 객체를 생성하여 사용한다고 가정한다.
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
}
아래 예시 코드는 아직 어노테이션을 사용하지 않았다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
public class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
memberRepository.clearStore();
}
@Test
void 회원가입() {
Member member = new Member();
member.setName("spring");
Long saveId = memberService.join(member);
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
중복_회원_예외() 테스트 메서드에서는 중복 회원인 경우-name이 같은 경우- 올바르게 예외를 처리하는지를 테스트한다.
회원 가입 시 이미 같은 name으로 가입한 회원이 있는 경우 "이미 존재하는 회원입니다."라는 문구가 뜨게 예외 처리를 했다. 중복_회원_예외() 메서드에서 일부러 중복 회원가입이 되게 하고, 예외 처리가 올바르게 되는지 테스트한다.
@AfterEach 어노테이션을 사용한 afterEach() 메서드가 존재하지 않았다면, 전체 클래스 테스트 실행 시 실패가 발생할 것이다.
앞에서 봤던 예시 테스트 코드처럼 회원가입() 메서드와 중복_회원_예외() 메서드 모두에서 spring이라는 이름을 가진 Member를 생성한다. 두 번째 실행되는 테스트 메서드에서 이미 spring이라는 이름을 가진 member1 인스턴스의 주소 값을 가져오기 때문이다.
별다른 설정을 하지 않은 경우 테스트 메서드는 abc(가나다) 순으로 실행되기 때문에 중복_회원_예외() 메서드가 회원가입() 메서드보다 먼저 실행된다.
테스트 실패를 없애기 위해 @AfterEach 어노테이션을 사용한 afterEach() 메서드를 생성하였고, afterEach() 메서드에서 clearStore() 메서드를 사용하기 위해 MemoryMemberRepository 객체를 생성하여 memberRepository에 저장하였다.
그러나 이는 한 가지 문제가 있는데 MemberService 클래스에서 생성한 MemoryMemberRepository 객체와 MeberServiceTest 클래스에서 생성한 MemoryMemberRepository 객체가 다르다는 것이다.
그래서 다른 인스턴스를 사용하는 것이 아닌, 한 개의 인스턴스를 가지고 MemberService 클래스와 MemberServiceTest 클래스에서 사용하기 위해 MemberService.java를 수정하고 @BeforeEach 어노테이션을 사용한다.
MemberService.java에서 DI를 사용해 MemberRepository를 저장한다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
테스트 클래스에서 MemberService와 MemoryMemberRepository 객체 생성 부분을 아래와 같이 수정하고, beforeEach() 메서드를 추가해준다.
import org.junit.jupiter.api.BeforeEach
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
}
이와 같이 설정한다면 테스트 시, 테스트 메서드에서 각각의 MemoryMemberRepository와 MemberService를 생성한다.
또한 한 개의 MemberRepository 인스턴스가 MemberService 클래스와 MemberServiceTest 클래스에서 사용된다.
이를 통해 각 테스트는 독립적으로 실행될 수 있다.
[인프런] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 (김영한)