데이터: 회원 ID, 이름
기능: 회원 등록/조회
아직 데이터 저장소가 선정되지 않음 (가상 시나리오)
Optional<>
: 비어있다면 null이 반환되는데 이때 optional로 감싸 반환되는 것을 선호함
package hello.hellospring.repository;
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); // map에 저장시킴
return member;
}
@Override
public Optional<Member> findById(Long id) { // store에서 꺼내오면 됨
return Optional.ofNullable(store.get(id)); // null이어도 감쌀 수 있음
// 감싸서 반환해주면 클라이언트가 뭘 할 수가 있어짐
}
@Override
public Optional<Member> findByName(String name) {
// 루프를 돌면서 찾으면 반환해주고 없으면 optional에 null을 감싸 반환
return store.values().stream()
.filter(member -> member.getName().equals(name)) // 파라미터로 넘어온 이름과 같은지 확인
.findAny(); // 그리고 찾으면 반환해주는
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
JUnit이라는 프레임워크로 테스트를 실행해 여러 테스트를 한 번에 실행
package hello.hellospring.repository;
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); // map에 저장시킴
return member;
}
@Override
public Optional<Member> findById(Long id) { // store에서 꺼내오면 됨
return Optional.ofNullable(store.get(id)); // null이어도 감쌀 수 있음
// 감싸서 반환해주면 클라이언트가 뭘 할 수가 있어짐
}
@Override
public Optional<Member> findByName(String name) {
// 루프를 돌면서 찾으면 반환해주고 없으면 optional에 null을 감싸 반환
return store.values().stream()
.filter(member -> member.getName().equals(name)) // 파라미터로 넘어온 이름과 같은지 확인
.findAny(); // 그리고 찾으면 반환해주는
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
이런 에러를 방지하기 위해 Aftereach
메소드 작성
(테스트가 하나 끝나면 데이터를 clean 시켜주도록 함)
모든 테스트는 동작 순서가 보장되지 않기 때문에 순서에 의존해 설계하면 절대 안됨.
@AfterEach
: 한 번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트 결과가 남아있을 수 있다. 그럼 그 잔여물 때문에 다음 진행할 테스트에 실패할 수 있다.
@AfterEach
를 사용하면 각 테스트가 종료될 때마다 해당 메소드를 실행한다. 이 예제에서는 DB를 비워주었기 때문에 다음 테스트도 성공할 수 있었다.
command + option + m
: 메소드로 뽑아내기
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) {
// optional이기 때문에 ifPresent 이런 값도 사용할 수 있는 것. (null이라면 이런 문을 안써도 됨)
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);
}
}
테스트 메소드 이름은 과감하게 한글로 해도 됨.
그리고
given
,when
,then
을 지켜서 짜는 것을 추천테스트의 핵심은 예외잡기. 예외상황 발생시 잘 동작하는지를 확인하는 것이 중요함
기존에는 회원 서비스가 메모리 회원 리포지토리를 직접 생성하도록 했으나 회원 리포지토리 소스가 회원 서비스 소스를 DI 가능하도록 변경했다.
@BeforeEach
: 각 테스트 실행 전 호출
테스트가 서로 영향받지 않도록 항상 새로운 객체를 생성하고 의존 관계를 새로 맺어줌.