해당 글은 DEPth IT 연합 프로젝트 동아리 활동의 일환인 서버 스터디에 관련되어 있습니다.🐧
테스트 코드
기능을 테스트하는 경우에 main 메서드를 통해서 실행하거나, 웹 어플리케이션의 컨트롤러를 통해서 해당 기능을 실행하는 것은 시간이 오래 걸릴 뿐만 아니라 반복 실행하기도 어렵고 여러 테스트를 한번에 진행하기도 힘들다는 단점이 있습니다. 자바는 이러한 문제들을 JUnit이라는 프레임워크를 이용해서 해결합니다.
테스트에서 똑같이 패키지를 만들고 테스트할 클래스 이름 + Test라고 class를 새로 만들어줍니다. 해당 클래스에서 이미 만든 class를 test합니다. 아래는 실제로 만든 테스트 코드입니다. 이전 3-1에서 작성한 MemoryMemberRepository 클래스를 테스트합니다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assert;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;
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();
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("spring1").get();
assertThat(result).isEqualTo(member1);
}
@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);
}
}
먼저 테스트 할 레포지포리 객체를 불러옵니다. 위의 코드에서는 repository라는 객체가 되었네요. 이후 @Test 어노테이션을 붙이면 해당 메서드를 테스트 할 수 있게 됩니다.
save 메서드에서는 spring이라는 이름을 저장하고 result에 값을 저장합니다. 저장될 때에 Optional로 저장되어서 get을 이용해서 꺼내는 모습입니다. 이후 assert를 이용해서 비교하는 두 값이 같은지 확인하고 있습니다. print를 이용해서 참, 거짓을 출력하게 해서 판별 가능하지만 모든 값을 출력하기에는 한계가 있습니다. 따라서 assert를 이용합니다. 이를 이용하면 테스트에서 옳지 않다고 나오게 되는 경우에 사용자에게 알려주게 됩니다.
이와 비슷한 내용으로 findByName, findAll 메서드도 Test합니다.
이때 각 메서드 별로 test도 가능하지만 class 별로 혹은 그 상위 단계에서도 test가 가능합니다. 이때 test되는 순서는 무작위 입니다. 따라서 순서가 필요하게 test 코드를 짜서는 안됩니다. 또한 위의 코드에서는 이미 같은 이름으로 save가 되었다면 이후에 test하는 메서드에 영향을 주어서 test 결과에 영향을 줍니다. 따라서 afterEach 어노테이션을 사용합니다. 해당 메서드는 한 메서드의 실행 후에 실행되는 메서드 입니다. 보통 data를 초기화하는 메서드를 입력합니다. 이를 통해서 중복 데이터를 신경 쓰지 않아도 됩니다.
⚙️ TDD?
TDD는 테스트 주도 개발로 소프트웨어 개발 방법론 중 하나입니다. 소프트웨어를 개발하는 경우 테스트 케이스를 먼저 작성하고, 후에 테스트 케이스를 통과시키는 코드를 개발합니다. 해당 접근 방식을 통해서 안정적인 코드를 개발 할 수 있으며, 품질을 높이고 유지보수성을 높일 수 있습니다. 초기에는 작은 단위의 기능을 구분하고 테스트 케이스를 작성하는 데에 익숙하지 않아 생산성이 떨어질 수 있지만, 시간이 지날 수록 코드의 안정성과 유지보수성이 좋아진다는 장점이 존재합니다.
회원 서비스 개발
실제로 비즈니스 로직이 들어가는 service 패키지를 만들고 이에 알맞은 클래스를 만듭니다. 이때 레포지토리 패키지에서 만든 클래스를 사용하여서 비즈니스 로직을 개발합니다. 레포지토리에서는 메서드의 이름 혹은 실제로 동작하는 로직도 조금은 데이터의 이동 및 기계적인 느낌이 강했다면 service의 클래스에서는 메서드 이름이나 로직에서도 비즈니스적인 요소가 보여야 합니다. 코드는 아래와 같습니다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
//회원 가입
public Long join(Member member){
//같은 이름의 중복 회원 X
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
//전체 회원 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
레포지토리에서 구현한 메서드들을 사용해서 실제로 회원 가입 기능, 회원 조회 기능을 구현하고 있습니다. 이때 join 메서드에서 중복 회원을 검증하는 메서드를 추가로 사용하고 있습니다. ifPresent 메서드는 optinal이 비어있지 않을 때만 동작합니다. 따라서 중복이 있으면 값이 있는 optinal이 있을 것이고 존재하는 회원이라는 문장이 출력되게 됩니다. 예외를 발생시키게 됩니다.
💡option + command + v로 각 알맞은 타입이 자동으로 입력됩니다.
서비스 테스트 케이스
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
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 memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void join() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
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("이미 존재하는 회원입니다.");
/*
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e){
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
*/
}
}
위와 같이 테스트 케이스를 진행합니다. 하지만 이때 beforeEach를 사용해서 다른 레포지터리를 사용하지 않고 같은 레포지토리를 사용하게 합니다. 또한 중복_회원_예외에서 try-catch 구문도 좋지만 assertThrows를 사용할 수도 있습니다.