Spring/Spring tutorial - 코드로 배우는 스프링 (김영한)

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

한둥둥 2022. 9. 17. 21:53

내가 이전에 작성했던 코드들이 정상적으로 작동을 할까에 대한 고민을 해결하기 위해서 우리는 테스트 케이스를 작성하여 검증할 것이다. 

 

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

개발한 기능을 실행해서 테스트 할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다.

이러한 방법은 준비하고 실행하는데 오래 걸리고, 반복 실행하기 어렵다. 또한 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 

자바는 이를 해결하기 위해 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결함. 

 

지금까지 나는 main method를 통해서 테스트를 수행하였다. 해당 기능을 테스트하기 위해  준비하고 테스트를 실행하는데 굉장히 오랜 시간이 걸린다. 이러한 이유 떄문에 TestCase 를 작성한다. 

repository 패키지와 MemoryMemberRepositoryTest라고 적어서 클래스를 하나 만들었다. 

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Test;

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();//->repository.findById(member.getId());
    }
}

MemoryMemberRespositoryTest에 이런식으로 작성을 해주었다. 

그러면 이 테스트 기능을 실행할 수 있다. 이전에 Run을 했던 것을 꺼주고 Run을 하면 Save()메서드가 실행된다. 

맥북 기준 : Command + shift + enter를 치면 바로 아래로 내려온다. (개꿀팁)

repository.save를 하면 자동으로 id가 하나 올라가면서 저장된다. 

여기서 findById를 하면서 반환 타입을 가져오는데 여기서 문제점은 우리는 Optional로 findByName을 반환타입으로 받아오기로 되어있다. 

그래서 findById안에 member.getId()이런식으로 가져오는 것이 아니라 Optional의 값을 가져올 때 findById(member.getId()).get()이런식으로 꺼내올 수 있다. 사실 꺼낼 때는 바로 꺼내오는게 좋은 방법은 아니지만 테스트 코드 같은 곳은 바로 꺼내와도 된다. 

이렇게 꺼내오고 검증을 해야됨 어떻게 검증을 하는가? 

new 에서 한거랑 db에서 꺼낸거랑 Member가 완전히 똑같으면 참이다.

 

package hello.hellospring.repository;

import ch.qos.logback.core.net.SyslogOutputStream;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Test;

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();
        System.out.println("result + " + (result == member) );
    }
}

조금 더 좋은 기능인 Asseortions라는 기능이 존재한다. -> org.junit.jupiter.api가 제공하는 것으로 Import하자.

package hello.hellospring.repository;

import ch.qos.logback.core.net.SyslogOutputStream;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

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(result, member);//<- 여기 이부분 
    }
}

정상적으로 나와서 체크 표시가 되는 것을 확인할 수 있다. 

Null값을 넣어서 실행해보자. 

위에 보면 기대 값이랑 다르게 null이 들어와서 x 표시가 된다. 

 

요즘에는 Assertions <- org.assertj.core.api를 사용 이게 좀 더 편하게 쓸 수 있게 해주는 아이다. 

package hello.hellospring.repository;

import ch.qos.logback.core.net.SyslogOutputStream;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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);
    }
}

Assrtions를 static import를 하면 Assertions를 없앨 수도 있다.

import static org.assertj.core.api.Assertions.*;
assertThat(member).isEqualTo(result);

Assertions 에서 option + enter 눌러서 static import 해주었다. 그러면 바로 assertThat으로 입력이 가능하다. 

실무에서는 빌드 툴에서 빌드를 할 때 테스트 툴에서 통과하지 않으면 다음단계로 넘어갈 수 없도록 막아 버린다. 

그리고 우리는 findByName을 테스트 할 것이다. 

 

shfit + F6을 누르면 변수명의 이름이 rename이 된다.  

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

            Member member2 = new Member();
            member2.setName("spring2");
            repository.save(member2);

            Member result = repository.findByName("spring2").get();
            assertThat(result).isEqualTo(member2);
    }

이런식으로 테스트 코드를 작성해주었다. 

사진 화면을 보듯이 정상적으로 테스트 코드가 작동하여 검증에 통과한 모습이다. test case의 장점은 다 같이 한번에 테스트 할 수 있다는 것에 장점이 있다. 

class를 눌러서 다 같이 한번에 확인할 수 있음 (엄청난 장점이다.)

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);
    }

이렇게 작성해주면 정상적으로 돌아간다. 하지만 class로 전체를 테스트해주었을 때 findByID가 에러가 나옴.

에러가 난 이유에 대해 알아보자. 

우선적으로 findAll이 findByName보다 먼저 실행하는 모습을 확인할 수 있다. 이 순서는 보장할 수 없다. 

모든 테스트는 순서와 관계없이 메소드 별로 따로 동작하도록 설계해야한다.

findAll을 할 때 spring1, spring2가 저장되어 버렸다. 그래서 findByName을 할 때 다른 객체가 findByName을 할 때 나오게 되버린 것이다. 

그래서 이걸 어떻게 해결해야 하나면 테스트가 하나씩 하나씩 실행될 때 하나가 끝날 때마다 데이터가 깔끔하게 클리어 되어야 한다. 

그래서 MemoryMemberRespository에서 테스트가 하나 끝날 때마다 repository를 초기화 해주는 코드를 넣어주어야 한다. 

@AfterEach
    public void afterEach(){

    }

@AfterEach -> 이것은 뭐냐면 테스트 코드의 각각의 메서드가 실행이 한번씩 끝날 때 마다 어떤 동작을 하는 것이다. 콜백 메서드라고 보면 된다. 

save끝나고 AfterEach, findByName이 끝나고 AfterEach, findAll 이 끝나고 AffterEach

package hello.hellospring.repository;

import ch.qos.logback.core.net.SyslogOutputStream;
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 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("spring2").get();
            assertThat(result).isEqualTo(member2);
    }
    @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);
    }
}

MemoryMemberRepository에 있는 store를  save를 통해서 Map에 저장해주는데 해당하는 코드들에서 이떄 repository.cleanStore()를 통해서 repository가 다 비어지게 된다. @AffterEach를 통해서 계속 비워주면서 테스트해주는 것임 개꿀인거같음. 

findAll(), findByName(), save()모두다 녹색 불이 뜬것을 확인할 수 있다. 

 

다시 한번 강조하지만 테스트는 서로간의 의존 관계가 없어야함 그러기 위해서는 데이터 저장소나 공용데이터를 싹 다 비워줘야함. 

MemoryMemberRepository를 먼저 개발을 한다음에 쭉 개발을 다 끝낸 후 그 다음에 테스트를 작성을 하였다.

그런데 이거를 완전 뒤집어서 테스트케이스를 먼저 작성한 다음에 MemoryMemberRespository를 작성할 수도 있다. 마치 내가 무언갈 만들어야 하는데 이걸 검증할 수 있는 틀을 만들어야함

예를들어서 별모양 작품을 만든다 했을 때 미리 별모양을 만들어놓고 꽂히는지 안꽂히는지 확인하는데 이것을 TDD라고 한다.

테스트가 수천가지일 때 Run을 패키지에 돌려서 하거나 Gradle W 띄우고 돌려서 테스트하면된다.