포스트

테스트 코드를 이용한 객체 동작 검증 방법 및 관련 개념

강의 내용 복습 : 김영한의 스프링 입문 강의 (섹션 3)

  • Tool :
    IntelliJ Spring Boot


🔔 강의 내용

📌 Java에서의 테스트 코드 작성

  • Java에서 개발한 웹 애플리케이션 기능은 컨트롤러의 메인 메서드에서 실행됩니다.
  • 이와는 달리 테스트 코드는 웹 애플리케이션의 기능을 반복 또는 다중 실행하기 위해 사용됩니다.
  • 기능 테스트 과정에서 버그 또는 변경사항에 따른 사이드 이펙트를 조기에 발견할 수 있습니다.
  • Java의 대표적인 테스트 프레임워크는 JUnit의 Test API가 있습니다.

📌 MemoryMemberRepository 객체 테스트

  • 메모리 공간이 인터페이스와 잘 연결되었는지, 코드가 잘 동작되는지 확인해야 됩니다.
  • 이를 위해 Member 객체와 JUnit의 Test, AfterEach, BeforeEach API가 사용되었습니다.
  • Member 객체의 name 데이터에 임의의 이름으로 member1, member2를 저장하였습니다.
  • 최종적으로 데이터를 리스트화 시키고 데이터의 개수를 파악하였습니다.
  • MemoryMemberRepository 객체의 테스트 코드에 대한 상세한 설명은 글 하단에 작성하였습니다.

📌 MemberService 객체 생성

  • 회원 관리를 위한 비즈니스 로직 내용을 포함한 서비스 객체를 생성하였습니다.
  • MemberService 객체는 MemberRepository 인터페이스에 의존합니다.
  • 그리고 해당 의존성은 생성자 메서드를 통해 외부에서 주입(DI)되게 설정하였습니다.
  • DI의 일환으로 MemberService 객체 생성 시 MemberRepository에 할당하도록 설정하였습니다.
  • MemberService 객체에 대한 상세한 설명은 글 하단에 작성하였습니다.

📌 MemberService 객체 테스트

  • MemberService 객체의 테스트 코드에 대한 상세한 설명은 글 하단에 작성하였습니다.


🔔 개념 보충

📌 Dependency Injection

  • 의존성 주입 관련된 개념은 다음 블로그 글에 작성하였습니다.
  • 해당 링크로 들어가시면 @Controller, @Service, @Repository에 대한 설명이 있습니다.
  • 또한 스프링 구축 시 @Component 및 @Autowired의 관계에 대한 내용도 있습니다.

📌 @AfterEach

  • JUnit 라이브러리의 대표 메서드 중 하나입니다.
  • 테스트 간의 충돌 방지를 위해 테스트 후 저장소를 비우도록 설정하는 메서드입니다.
  • 이는 테스트가 실행된 이후마다 적용되며 테스트 코드끼리 서로 의존하지 않게 됩니다.

📌 @BeforeEach

  • JUnit 라이브러리의 대표 메서드 중 하나입니다.
  • 테스트가 실행될 때마다 새로운 메서드가 생성되도록 설정할 수 있는 메서드입니다.
  • 생성된 객체의 인스턴스를 다른 객체에 의존성 주입으로 제공하도록 설정할 수 있습니다.
  • BeforeEach 과정을 거치면 각 테스트가 실행될 때 데이터가 중복되지 않게 됩니다.

📌 assertThat

  • AssertJ 라이브러리의 메서드 중 하나입니다.
  • assertThat 메서드는 실제값과 기댓값을 비교하는 데 사용됩니다.
  • 메서드 사용 방법의 예시로는 assertThat(actual).isEqualTo(expected)가 있습니다.
  • 여기서 actual 값이 expected 값과 동일한지 비교합니다.

📌 isEqualTo

  • AssertJ 라이브러리의 메서드 중 하나로 보통 assertThat 메서드와 함께 사용됩니다.
  • 객체의 실체값이 기댓값과 일치하는지 검사하기 위해 사용됩니다.

📌 assertThrows

  • AssertJ 라이브러리의 메서드 중 하나입니다.
  • assertThrows 메서드는 특정 조건에서 예외적인 값이 발생되는지 검사하는 데 사용됩니다.
  • 메서드 사용 방법의 예시는 아래와 같습니다.
  • assertThrows(IllegalStateException.class, () -> { /* 테스트 코드 */ })
  • 예시의 테스트 코드 실행 시 IllegalStateException이 발생되는지 검사할 수 있습니다.

📌 IllegalStateException

  • 메서드가 적절하지 않은 호출을 받았을 때 발생되는 런타임 예외입니다.
  • 예를들어 아직 객체 초기화가 되지 않았거나 소비된 상태에서 사용하려고 시도할 때 발생됩니다.
  • 따라서 메서드가 요구하는 객체의 상태를 충족하는지에 대한 여부를 검사할 수 있습니다.


🔔 로직 이해

📌 MemoryMemberRepository 객체의 테스트 코드

  • 아래는 MemoryMemberRepositoryTest의 전체 코드이며 관련 설명을 주석으로 작성하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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;

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

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    @AfterEach
    public void afterEach() {
        repository.clearStore();
    }
    // 테스트 간의 충돌을 방지하기 위해 테스트가 종료될 때마다 저장소를 비우도록 설정하였습니다.

    @Test
    public void save() {
        // given
        Member member = new Member();
        member.setName("Spring");
        // Spring이라는 name의 새로운 Member 객체를 생성하였습니다.

        // when
        repository.save(member);

        // then
        Member result = repository.findById(member.getId()).get();
        assertThat(result).isEqualTo(member);
        // Member 객체의 저장된 id와 조회한 id가 일치하는지 확인하였습니다.
    }

    @Test
    public void findByName() {
        // given
        Member member1 = new Member();
        member1.setName("Spring1");
        repository.save(member1);

        // given
        Member member2 = new Member();
        member2.setName("Spring2");
        repository.save(member2);
        // 다른 name 값을 가진 회원(member1, member2) 객체를 생성하고 저장소에 저장하였습니다.

        // when
        Member result = repository.findByName("Spring1").get();
        // 객체의 name 값이 Spring1인 Member 객체를 조회하였습니다.

        // then
        assertThat(result).isEqualTo(member1);
        // Spring1 name으로 조회된 객체가 member1인지 확인하였습니다.
    }

    @Test
    public void findAll() {
        // given
        Member member1 = new Member();
        member1.setName("Spring1");
        repository.save(member1);

        // given
        Member member2 = new Member();
        member2.setName("Spring2");
        repository.save(member2);
        //  name 값이 다른 2개의 회원 객체를 생성하였고 저장소에 저장하였습니다.

        // when
        List<Member> result = repository.findAll();
        // findAll 메서드로 저장소에 저장된 모든 회원의 리스트를 조회하였습니다.

        // then
        assertThat(result.size()).isEqualTo(2);
        // 조회된 리스트의 크기가 2인지 조회하였습니다.
    }
}

📌 MemberService 객체

  • 아래는 MemberService 객체의 전체 코드이며 관련 설명을 주석으로 작성하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package hello.hellospring.service;

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

import java.util.List;
import java.util.Optional;

public class MemberService {

    private final MemberRepository memberRepository;

    // MemberService 객체에 의존성을 주입할 수 있도록 변경
    public MemberService(MemberRepository memberRepository) {
    // MemberService 객체가 MemberRepository 인터페이스에 의존하도록 설정하였습니다.
    // 해당 의존성은 생성자 메서드를 통해 외부에서 주입되게 설정하였습니다.
        this.memberRepository = memberRepository;
    }

    // 회원가입 기능
    public Long join(Member member) {
        validateDuplicateMember(member);
        // 주어진 Member 객체의 데이터가 저장소에 이미 존재하는지 확인하도록 설정하였습니다.
        // 내부 메서드인 validateDuplicateMember에 대한 로직은 하단 문단에 작성하였습니다.
        memberRepository.save(member);
        // Member 객체의 데이터가 중복되지 않으면 MemberRepository를 통해 저장소에 Member 객체를 저장하도록 설정하였습니다.
        return member.getId();
        // 최종적으로 로직에 따라 저장된 Member의 id를 반환하도록 설정하였습니다.
    }

    // 저장소의 Member 객체 중복 여부 검증
    // 해당 검증 로직은 내부 메서드로 join 메서드 내에서 호출됩니다.
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
        // Member 객체의 저장소에 주어진 name이 있는지 검색합니다.
                .ifPresent(m -> {
                // 만일 저장소에 동일한 name의 Member 객체가 존재한다면 람다로 이어지게 설정하였습니다.
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                    // join 메서드에서 id를 반환하는 것이 아니라 IllegalStateException을 반환하도록 설정하였습니다.
                });
    }

    // 전체 회원 조회 기능
    public List<Member> findMembers() {
        return memberRepository.findAll();
        // 저장소의 findAll 메서드를 호출하여 전체 회원 리스트를 반환하게 설정하였습니다.
    }

    // 특정 회원 조회 기능
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
        // 저장소의 findById 메서드를 호출하여 memberId에 해당되는 Member 객체를 Optional 타입으로 반환하도록 설정하였습니다.
    }
}

📌 MemberService 객체의 테스트 코드

  • 아래는 MemberServiceTest의 전체 코드이며 관련 설명을 주석으로 작성하였습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

    // 각각의 테스트 실행 전 새로운 멤버 리포지토리 및 멤버 서비스 객체 생성
    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
        // 테스트가 실행될 때마다 새로운 MemoryMemberRepository를 생성하도록 설정하였습니다.
        // 생성된 MemoryMemberRepository 인스턴스를 MemberService에 의존성 주입으로 제공하도록 설정하였습니다.
        // 이 과정을 거치면 각 테스트 사이에 데이터가 중복되지 않게 됩니다.
    }

    // 각각의 테스트 실행 후 멤버 리포지토리에 저장된 내용 삭제
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
        // MemoryMemberRepository에 저장된 모든 데이터를 삭제하도록 설정하였습니다.
    }

    // 회원가입 시나리오 테스트
    @Test
    public void MemberJoin() throws Exception {
        // given
        Member member = new Member();
        member.setName("hello");
        // 새로운 Member 객체를 생성하고 이름을 설정하도록 하였습니다.

        // when
        Long saveId = memberService.join(member);
        // join 메서드는 Member 객체를 매개변수로 전달받습니다.
        // 전달받은 Member 객체를 MemoryMemberService에 저장합니다.
        // Member 객체 저장이 성공적으로 완료되면 id가 반환됩니다.
        // 반환된 id는 long 타입의 saveId 변수에 저장됩니다.

        // then
        Member findMember = memberRepository.findById(saveId).get();
        // saveId 변수에 저장된 Member의 id로 회원 정보를 조회합니다.
        assertEquals(member.getName(), findMember.getName());
        // 입력한 Member의 name과 저장 후 조회된 회원 name이 일치하는지 검토합니다.
    }

    // 중복 회원 예외 처리 시나리오 테스트
    @Test
    public void ExceptionDuplicatedMember() throws Exception {
        // given
        Member member1 = new Member();
        member1.setName("spring");

        // given
        Member member2 = new Member();
        member2.setName("spring");
        // 동일한 name을 가진 member1, member2 변수를 생성하였습니다.

        // when
        memberService.join(member1);
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));
        // 첫 번째 회원인 member1은 정상적으로 회원가입시킵니다.
        // 하지만 member2의 경우 회원가입 시 IllegalStateException이 발생되는지 검사합니다.

        // then
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
        // 중복 검사 결과 예외가 발생되었을 때 발생된 메시지가 "이미 존재하는 회원입니다."와 일치하는지 조회하도록 설정하였습니다.
        // 이는 중복 회원 가입 시 예외 처리가 올바르게 동작하는지 검증하는 것입니다.
    }
}




이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.
<>