[Spring] 3_2. 회원 관리 예제 - 회원 레포지토리 테스트 케이스 작성 / 서비스 개발 / 서비스 테스트

2025. 4. 25. 18:04·BE/스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술
728x90

회원 레포지토리 테스트 케이스 작성

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

public class MemoryMemberRepositoryTest {

    MemberRepository repository = new MemoryMemberRepository();

    @Test
    public void save(){
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        Member result = repository.findMyId(member.getId()).get();
        assertThat(member).isEqualTo(result);
    }
}

@Test

스프링부트에서 테스트코드는 주로 프레임워크에서 test 코드를 작성하는 디렉토리인 src/test/java에 작성한다.

@Test
@DisplayName("member와 result가 일치하면 성공")
public void save(){
    Member member = new Member();
    member.setName("spring");

    repository.save(member);

    Member result = repository.findMyId(member.getId()).get();
    assertThat(member).isEqualTo(result);
}

@Test 어노테이션을 사용해서 테스트 메서드를 정의해주는데, 

각 테스트 메서드는 테스트 케이스를 나타내고 테스트할 기능을 작성한다.

@DisplayName

@Test
@DisplayName("member와 result가 일치하면 성공")
public void save(){
    ...
}

@Displayname 어노테이션은 테스트 메서드의 이름을 지정해준다.

MemoryMemberRepositoryTest를 실행하면 각 테스트 메서드가 실행되는데, 이때 어떤 테스트 메서드인지 이름을 지정해줄 수 있다.

Assertions

개발자가 테스트하고 싶은 인자값을 넣었을 때 예상한 결과가 나오는지 테스트 해볼 경우 사용

Member result = repository.findMyId(member.getId()).get();
assertThat(member).isEqualTo(result);

// -----------------------------

List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(3);

assertThat 안에 넣은 값과, isEqualTo 안에 넣은 값이 같은지 비교한다.

만약 비교대상이 같지 않을 경우 이렇게 failed 라고 뜬다.

코드 실행시 clear를 해주지 않을 경우

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @Test
    @DisplayName("member와 result가 일치하면 성공")
    public void save(){
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        Member result = repository.findMyId(member.getId()).get();
        assertThat(member).isEqualTo(result);
    }

    @Test
    @DisplayName("이름을 제대로 찾으면 성공")
    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.findMyName("spring1").get();

        assertThat(result).isEqualTo(member1);
    }
    
    @Test
    @DisplayName("저장된 객체의 수가 N개면 성공")
    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);
    }
}

 

자 그럼 테스트 코드를 다 짰으니 이제 이 클래스를 동작시켜보자.

어? failed가 뜬다.

save() 와 findByName() findAll() 메서드를 하나씩 실행시켰을 때에는 문제가 없었는데?

private static Map<Long, Member> store = new HashMap<>();

우리는 지금 MemoryMemberRepository 클래스에서 위와 같이 store라는 공용 메모리 공간을 가지고 있다.

테스트코드는 모두 repository 인스턴스를 공유하고 있기 때문에,

테스트 각각에서 저장한 데이터가 다음 테스트에서도 그대로 남아있을 수 있다.

public void clearStore(){
    store.clear();
}

MemoryMemberRepository 클래스에서 store를 clear할 수 있는 메서드를 만든 뒤,

@AfterEach

@AfterEach
public void afterEach(){
    repository.clearStore();
}

@AfterEach : 한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게되면 다음 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다.`@AfterEach` 를 사용하면 각 테스트가 종료될 때 마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다.

 

@AfterEach 어노테이션을 사용해 각 테스트가 끝날 때마다 afterEach() 메서드가 실행되게 만들어야 한다.

그러면 repository 인스턴스 내의 clearStore()메서드가 실행되어 store가 각 테스트 끝날 때마다 clear된다.

 전체코드

package hello.hello_spring.repository;

import hello.hello_spring.domain.Member;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.util.List;

public class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }

    @Test
    @DisplayName("member와 result가 일치하면 성공")
    public void save(){
        Member member = new Member();
        member.setName("spring");

        repository.save(member);

        Member result = repository.findMyId(member.getId()).get();
        assertThat(member).isEqualTo(result);
    }

    @Test
    @DisplayName("이름을 제대로 찾으면 성공")
    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.findMyName("spring1").get();

        assertThat(result).isEqualTo(member1);
    }
    @Test
    @DisplayName("저장된 객체의 수가 N개면 성공")
    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);
    }
}
즉, 테스트는 서로 순서와 의존관계 없이 설계되어야 한다.
그러기 위해선 하나의 테스트가 끝날 때마다 저장소나 데이터들을 지워줘야 문제가 없다.

회원 서비스 개발

package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;

import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 회원 가입
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원은 가입 안됨
        // command+option+v 누르면 자동으로 Optional 지정
        Optional<Member> result = memberRepository.findMyName(member.getName());
        // result가 존재하는지 확인
        result.ifPresent(m -> {
            // 존재하면 이미 존재하는 회원으로 예외처리
            throw new IllegalStateException("이미 존재하는 회원입니다."); 
        });
        memberRepository.save(member);
        return member.getId();
    }
}

 

같은 이름이 있는 중복 회원은 가입이 불가능할 때

// 같은 이름이 있는 중복 회원은 가입 안됨
memberRepository.findMyName(member.getName());

memberRepository안에 member.getName()과 같은 이름이 있는지 확인해야한다.

// command+option+v 누르면 자동으로 Optional 지정
Optional<Member> result = memberRepository.findMyName(member.getName());

command + option + v 누르면 자동으로 Optional이 지정된다.

Optional <T> ifPresent()

    public Long join(Member member){
        // 같은 이름이 있는 중복 회원은 가입 안됨
        // command+option+v 누르면 자동으로 Optional 지정
        Optional<Member> result = memberRepository.findMyName(member.getName());
        // result가 존재하는지 확인
        result.ifPresent(m -> {
            // 존재하면 이미 존재하는 회원으로 예외처리
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
        memberRepository.save(member);
        return member.getId();
    }

ifPresent()는 Optional 객체가 값을 가지고 있으면 실행, 값이 없으면 넘어감

result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
  • isPresent() 메소드 = true, false 체크
  • ifPresent() 메소드 = 값을 가지고 있는지 확인 후 예외처리
cf) orElse() 사용하면 값이 없으면 기본값(객체)을 리턴한다

 

memberRepository.findMyName(member.getName())
    .ifPresent(m -> {
    throw new IllegalStateException("이미 존재하는 회원입니다.");
});

Optional<T> 사용한 뒤, isPresent를 적용한 코드는 너무 길기 때문에 이렇게 줄여서 사용할 수도 있다.

근데! 이런 로직은 메서드로 빼서 사용하는게 보기 좋고 편할 것 같은데?

control+T -> method 검색 후, Extract Method

package hello.hello_spring.service;

import hello.hello_spring.domain.Member;
import hello.hello_spring.repository.MemberRepository;
import hello.hello_spring.repository.MemoryMemberRepository;

import javax.swing.text.html.Option;
import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    // 회원 가입
    public Long join(Member member){
        // 같은 이름이 있는 중복 회원은 가입 안됨
        // command+option+v 누르면 자동으로 Optional 지정
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findMyName(member.getName())
            .ifPresent(m -> {
            // 존재하면 이미 존재하는 회원으로 예외처리
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }

    // 전체 회원 조회
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findMyId(memberId);
    }
}

서비스는 비즈니스에 의존적으로 설계해야한다. ex) 회원가입: join / 전체 회원 조회: findMembers


회원 서비스 테스트

이제 회원 서비스를 테스트해볼 예정이다. 이전에는 src/test/java 아래에 repository라는 package를 만들고, MemoryMemberRepositoryTest 클래스를 만들어서 테스트 코드를 직접 작성했다. 

서비스 자동 생성

위에서 작성한 서비스 코드인 MemberService에서 command+shift+T 단축키로 Create new Test 해주면 위와 같은 창이 뜬다. 내가 생성하고 싶은 메서드를 체크해주고 OK 누르면

이렇게 test폴더 내에 자동으로 패키지와 테스트 클래스를 생성해준다.

static import 단축키: option + enter

테스트 코드 작성시 주의해야할 점

테스트 코드를 작성할 땐, given / when / then 패턴으로 작성하는 것이 좋다.

이에 대해서는 추후에 다시 한번 JUnit과 TDD에 대해 작성하면서 함께 공부해볼 계획이다.

 

간단히 말하면 given-when-then 패턴은 TDD 주도 개발에서 많이 쓰이는 패턴으로,

  • given : 테스트 실행을 준비하는 단계
  • when : 테스트를 진행하는 단계
  • then : 테스트 결과를 검증하는 단계

와 같이 세 단계로 구분해 작성하는 방식을 말한다.

class MemberServiceTest {

    MemberService memberService = new MemberService();
    @Test
    @DisplayName("회원가입")
    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 duplicateJoin(){
        //given
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //then
        try {
            memberService.join(member2);
            fail(); //member1 "spring"과 member2 "spring"이 충돌 => 실패
        } catch(IllegalStateException e){
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        }
    }
}

중복된 회원이 가입 신청한다면 IllegalStateException이 발생할 것이다. 이걸 처리하기 위해 try - catch문을 사용할 수도 있다.

assertThat을 사용해 exception에서 발생한 메시지가,

private void validateDuplicateMember(Member member) {
    memberRepository.findMyName(member.getName())
        .ifPresent(m -> {
        // 존재하면 이미 존재하는 회원으로 예외처리
        throw new IllegalStateException("이미 존재하는 회원입니다.");
    });
}

서비스에서 IllegalStateException으로 던진 "이미 존재하는 회원입니다"와 같을 경우 catch로 처리해줄 수도 있다.

Assertions.assertThrows()

//then
memberService.join(member1);
IllegalStateException e = Assertions.assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

Assertions.assertThrows() 메서드는 JUnit5에서 예외가 발생하는지 테스트할 때 사용하는 메서드다.

이 메서드를 사용해 예외가 발생하는지 테스트해볼 수 있다.

Assertions.assertThrows(IllegalStateException.class, () -> memberService.join(member2));

그리고 예외가 발생했는지, 아닌지를 메시지를 통해 확인할 수도 있다.

메시지를 통해 확인하기 위해서는 Assertions.assertThrows를 IllegalStateException e 에 담고,

assertThat을 사용해 메시지와 동일한지 확인한다.

@BeforeEach

@BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존관계도 새로 맺어준다.

728x90

'BE > 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 카테고리의 다른 글

[Spring] 5. 회원관리 예제(웹 MVC 개발)  (3) 2025.04.29
[Spring] 4. 스프링 빈과 의존관계  (1) 2025.04.29
[Spring] 3_1. 회원 관리 예제 - 비즈니스 요구사항 정리 / 회원 도메인과 레포지토리 생성  (1) 2025.04.19
[Spring] 2_2. 스프링 웹 개발 기초  (0) 2025.04.19
[Spring] 2_1. 프로젝트 환경설정  (0) 2025.04.18
'BE/스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술' 카테고리의 다른 글
  • [Spring] 5. 회원관리 예제(웹 MVC 개발)
  • [Spring] 4. 스프링 빈과 의존관계
  • [Spring] 3_1. 회원 관리 예제 - 비즈니스 요구사항 정리 / 회원 도메인과 레포지토리 생성
  • [Spring] 2_2. 스프링 웹 개발 기초
DROPDEW
DROPDEW
💻 Developer | 기록하지 않으면 존재하지 않는다
  • DROPDEW
    제 2장 1막
    DROPDEW
  • 전체
    오늘
    어제
    • categories (408) N
      • App/Android (1)
      • BE (41) N
        • HTTP 웹 기본 지식 (8)
        • 스프링 입문 - 코드로 배우는 스프링 부트, 웹 .. (12)
        • 스프링부트와 JPA 활용 (8) N
        • 스프링부트 시큐리티 & JWT (0)
        • PHP (6)
      • FE·Client (23)
        • HTML (1)
        • React (19)
        • Unity (1)
      • Data (12)
        • AI (4)
        • Bigdata (6)
        • Database (1)
        • 빅데이터분석기사 (0)
      • Infra (0)
      • CS (7)
        • CS 면접 준비 (3)
      • 취준 (13)
        • 자격증·인턴·교육 (4)
        • 인적성·NCS (6)
        • 코테·필기·면접 후기 (3)
      • 코테 (270) N
        • Algorithm (222) N
        • SQL (35)
        • 정리 (13)
      • 인사이트 (27)
        • 금융경제뉴스 (7)
        • 금융용어·지식 (2)
        • 북마크 (7)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    그래프이론
    그래프탐색
    브루트포스 알고리즘
    다이나믹프로그래밍
    티스토리챌린지
    백준
    너비우선탐색
    오블완
    이분탐색
    투포인터
    최단경로
    정렬
    매개변수탐색
    누적합
    수학
    문자열
    자료구조
    구현
    시뮬레이션
    그리디알고리즘
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.3
DROPDEW
[Spring] 3_2. 회원 관리 예제 - 회원 레포지토리 테스트 케이스 작성 / 서비스 개발 / 서비스 테스트
상단으로

티스토리툴바