-
인프런 스프링 입문 05 / 회원 관리 예제, junit 테스트하는 방법스프링&스프링부트 2022. 12. 4. 14:28
스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
- 데이터 : 회원ID, 이름
- 기능 : 회원 등록, 조회
- 아직 데이터 저장소 선정되지 않음(가상 시나리오)
- 일반적인 웹 어플리케이션 계층 구조
> 컨트롤러 : 웹 MVC의 컨트롤러 역할
> 서비스 : 핵심 비즈니스 로직 구현
> 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 동작하도록 구현한 계층
> 예) 회원은 중복 가입이 안 된다
> 리포지토리 : 데이터베이스 접근, 도메인 객체들 DB에 저장하고 관리
> 도메인 : 비즈니스 도메인 객체
> 예) 회원, 주문, 쿠폰 등 주로 데이터베이스에 저장하고 관리
- 클래스 의존관계
> 아직 데이터 저장소가 선정되지 않아 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
> 데이터 저장소는 RDB, NoSQL 등등 다양한 저장소를 고민 중인 상황으로 가정
> 개발을 진행하기 위해서 초기 개발 단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
회원 도메인과 리포지토리 만들기
- 회원 객체
- getter, setter 윈도우 단축키 > alt + insert
1) domain > main
package hello.hellospring.domain; 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; } }
2) repository > MemberRepository
package hello.hellospring.repository; import hello.hellospring.domain.Member; import java.util.List; import java.util.Optional; public interface MemberRepository { Member save(Member member); // 저장소 저장 Optional<Member> findById(Long id); // id 이용 찾기 Optional<Member> findByName(String name); // 이름 이용 찾기 List<Member> findAll(); //찾기 }
3) repository > MemoryMemberRepository
package hello.hellospring.repository; import com.fasterxml.jackson.annotation.JsonTypeInfo; import hello.hellospring.domain.Member; import java.util.*; 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)); // Optional.ofNullable() > null이어도 감싸서 보낼 수 있음 } @Override public Optional<Member> findByName(String name) { return store.values().stream().filter(member -> member.getName().equals(name)).findAny(); } @Override public List<Member> findAll() { return new ArrayList<>(store.values()); } }
회원 리포지토리 테스트 케이스 작성
- 개발한 기능을 테스트할 때 자바의 main 메서드를 통해 실행, 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행
> 준비하고 실행하는데 오래 걸리고 반복 실행이 어려움
> java는 Junit이라는 프레임워크로 테스트 실행
- 회원 리포지토리 메모리 구현 테스트
- test > java > hello.hellospring > MemoryMemberRepositoryTest
1) save 검증 1 (System.out.println)
package hello.hellospring.repository; import hello.hellospring.domain.Member; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; class MemoryMemberRepositoryTest { MeomeryMemberRepository repository = new MemoryMemberRepository(); @Test public void save() { Member member = new Member(); member.setName("spring"); repository.save(member); Member result = repository.findById(member.getId()).get(); System.out.println("result = " + (result == member)); } }
정상 작동 2) save 검증 2 (org.junit.jupitet.api)
class MemoryMemberRepositoryTest { MemberRepository repository = new MemoryMemberRepository(); @Test public void save() { Member member = new Member(); member.setName("spring"); repository.save(member); Member result = repository.findById(member.getId()).get(); Assertions.assertEquals(member, result); //Assertions.assertEquals(member, null); } }
assertEquals(member, result) > console에 아무것도 찍히지 않지만 정상 작동 assertEquals(member, null) > 오류 발생 3) save 검증 3 (org.assertj.core.api)
class MemoryMemberRepositoryTest { MemberRepository repository = new MemoryMemberRepository(); @Test public void save() { Member member = new Member(); member.setName("spring"); repository.save(member); Member result = repository.findById(member.getId()).get(); Assertions.assertThat(member).isEqualTo(result); } }
정상 작동 4) findByName 검증
@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(member2); }
출력없음 > 정상 작동 값이 틀린 경우 오류 발생 5) findAll 검증
@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); }
6) class 검증 > 모든 메소드 한 번에 검증 가능
> 테스트 할 때 순서 보장 안 됨
> 순서와 상관없이 메서드 별로 테스트되도록 설계
>> 메소드 별로 test 끝나면 데이터 clear
6-1) 순서와 상관없이 메서드 별로 테스트 설계
6-1-1) MemoryMemberRepository에 clearStore 메서드 생성
public class MemoryMemberRepository implements MemberRepository{ public void clearStore() { store.clear(); } }
6-1-2) MemoryMemberRepositoryTest에 afterEach() 메서드 생성
> @AfterEach 사용으로 test 끝날 때마다 데이터 clear
class MemoryMemberRepositoryTest { MemoryMemberRepository repository = new MemoryMemberRepository(); @AfterEach public void afterEach() { repository.clearStore(); } }
순서에 관계없이 테스트 완료 * 테스트 틀을 먼저 만들고 구현 클래스 만드는 방법 > 테스트 주도 개발(TDD)
- TDD 장점
- testcode 개발의 필요성
회원 서비스 개발
- hello.hellospring > service > MemberService
1) 회원 가입 > 중복된 이름 가입 불가 > null 가능성이 있는 경우 optional + ifPresent 사용
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 { public final MemberRepository memberRepository = new MemoryMemberRepository(); //회원 가입 public long join(Member member) { //같은 이름이 있는 중복 회원 x Optional<Member> result = memberRepository.findByName(member.getName()); result.ifPresent(m -> {throw new IllegalStateException("이미 존재하는 회원입니다.");}); memberRepository.save(member); return member.getId(); } }
2) 바로 ifPresent 사용
public class MemberService { public final MemberRepository memberRepository = new MemoryMemberRepository(); //회원 가입 public long join(Member member) { //같은 이름이 있는 중복 회원 x memberRepository.findByName(member.getName()) .ifPresent(m -> { throw new IllegalStateException("이미 존재하는 회원입니다."); }); memberRepository.save(member); return member.getId(); } }
3) 메소드로 추출, 전체 회원 조회
> 메서드 추출 단축키 : Ctrl + Alt + M
public class MemberService { public final MemberRepository memberRepository = new MemoryMemberRepository(); //회원 가입 public long join(Member member) { //중복 회원 검증 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); } }
회원 서비스 테스트
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.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 = new MemberService(); MemoryMemberRepository memberRepository = new MemoryMemberRepository(); @Test void 회원가입() { //given Member member = new Member(); member.setName("spring"); //when Long saveId = memberService.join(member); //then Member findMember = memberService.findOne(saveId).get(); assertThat(member.getName()).isEqualTo(findMember.getName()); }
- test > given, when, then 사용
- test할 때 메서드 이름은 한국어로 가능, build 할 때 코드 포함되지 않음
- 예외도 항상 test해야함
- 예외 상황 확인 방법 1 (try,catch 이용)
@Test public void 중복_회원_예외() { Member member1 = new Member(); member1.setName("spring"); Member member2 = new Member(); member2.setName("spring"); memberService.join(member1); try { memberService.join(member2); fail(); } catch (IllegalStateException e) { //예외가 정상적으로 실행 assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); } }
- 예외 상황 확인 방법 2
@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("이미 존재하는 회원입니다."); }
- 테스트 독립적으로 만들기 > AfterEach
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("이미 존재하는 회원입니다."); } @Test void findMembers() { } @Test void findOne() { } }
- 문제 발생 : MemberService에서 실제 사용하는 memberRepository와 test에서 사용하는 MemberRepository가 다름
(인스턴스가 다름)
> MemberServie와 MemberServicTest 모두 수정 필요
1-1) MemberService 수정 전
public class MemberService { public final MemberRepository memberRepository = new MemoryMemberRepository(); //회원 가입 public long join(Member member) { validateDuplicateMember(member); memberRepository.save(member); return member.getId(); } }
1 - 2) MemberService 수정 후
- memberRepository를 new 사용해 직접 생성하는 것이 아니라 외부에서 가져오도록 수정
public class MemberService { private final MemberRepository memberRepository; public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } //회원 가입 public long join(Member member) { validateDuplicateMember(member); memberRepository.save(member); return member.getId(); } }
2) MemberServicTest 수정 > beforeEach 사용
class MemberServiceTest { MemberService memberService; MemoryMemberRepository memberRepository; @BeforeEach public void beforeEach() { memberRepository = new MemoryMemberRepository(); memberService = new MemberService(memberRepository); } @AfterEach public void afterEach() { memberRepository.clearStore(); } }
- test 시행할 때마다 "memberRepository = new MemoryMemberRepository"에서 memoryMemberRepository 생성
- memberService에게 전달
- 결론적으로 같은 memoryMemberRepository 사용 가능
> D.I
728x90'스프링&스프링부트' 카테고리의 다른 글
인프런 스프링 입문 07 / 회원관리 예제 (1) 2022.12.06 인프런 스프링 입문 06 / 스프링 빈 등록 2 가지 (0) 2022.12.04 JSON이란? JSON 기초 정리 (1) 2022.12.03 인프런 스프링 입문 03 / 정적 콘텐츠, MVC (0) 2022.12.02 인프런 스프링 입문 02 / 간단한 실행 (0) 2022.12.01