Spring Boot/스프링 입문 강의

Spring 입문 Chapter 3-3. 회원 관리 예제 : 회원 리포지트리 테스트 케이스 작성

HEY__ 2022. 2. 11. 12:10
728x90

이 글은 인프런에 있는 김영한님의 "스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술" 강의를 듣고 정리한 필기입니다.

 

 

⛅️ JUnit을 이용한 테스트 코드 작성

이번 시간에는 저번에 구현한 기능들이 제대로 작동하는지 확인하는 테스트를 해볼 것이다.

 

테스트를 해볼 수 있는 방법을 생각해보자. 당장 생각나는 방법은 두 가지이다.

1. java의 main 메서드를 통해서 실행

2. 웹 어플리케이션의 컨트롤러를 통해서 해당 기능을 실행

하지만 이 방법들은 준비하고 실행하는데 오래 걸리고, 반복적으로 실행하기가 어렵다는 단점이 있다.

 

java에서 제공하는 JUnit이라는 프레임 워크로 테스트 코드를 작성함으로써 위의 단점들을 해결할 수 있다.

그럼 이제 JUnit 프레임 워크를 사용하여 테스트 코드를 작성해보자.

 


⛅️ 기능을 테스트 할 Class 만들기

먼저 테스트 코드를 작성할 Class를 만들어야 한다.

이번에 우리가 테스트 해볼 기능은 MemoryMemberRepository의 기능들 (save, findById, findByName, findAll)이다.

보통 테스트 코드를 작성할 때, 테스트 대상이 되는 것들의 이름은 그대로 따라가고 class의 경우 뒤에 Test를 붙여주는 것이 관례이다.

 

따라서 test 폴더에 repository package를 생성하고, 해당 package 밑에 MemoryMemberRepositoryTest라는 이름의 클래스를 생성한다.

test 폴더에 클래스 생성

그리고 우리는 MemoryMemberRepository의 기능들의 작동을 확인 할 것이므로, MemoryMemberRepository 객체를 하나 만들어준다.

class MemoryMemberRepositoryTest {
    // 테스트 할 기능이 담긴 객체 생성
    MemoryMemberRepository repository = new MemoryMemberRepository();


}

 


⛅️ save 기능 테스트하기

이제 MemoryMemberRepository에서 구현한 save 기능이 제대로 작동하는지 확인하는 테스트 코드를 작성해보자.

우선 함수를 선언하고, 위에 @Test를 적어준다.

@Testjunit.jupiter.api에 포함된 메소드로써, 해당 메소드가 단위 테스트임을 명시해준다.

JUnit은 테스트 패키지 하위의 @Test 어노테이션이 붙은 메소드를 단위 메소드로 인식하여 독립적으로 실행할 수 있도록 한다.

class MemoryMemberRepositoryTest {
    MemberRepository repository = new MemoryMemberRepository();

    @Test
    public void save(){
    }
}

 

저장이 잘 되는지 확인하기 위해서

① member 객체를 생성하고 setName을 통해 이름을 spring이라고 지정한다.

save 메서드를 통해 생성한 member 객체를 저장한다.

③ 저장이 됐는지 확인을 하기 위해 member.getId에 해당하는 member객체를 findById를 통해 repository에서 찾아온다.

    그리고 Member형 객체 result에 저장한다.

    (시스템이 저장한 ID를 통해 다시 해당 ID로 설정된 member 객체를 찾아온다)

member와 result가 같다면 기능이 정상 작동한다는 것을 알 수 있을 것이다.

    두 객체가 같은지의 여부를 console 창에 출력한다. true라면 두 객체가 같다는 것을 뜻한다.

@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));
}
findById를 구현할 때 Optional을 반환하게끔 설정해놨다. 이 테스트 코드에서 member 객체를 받아서 비교를 해야 하기 때문에 .get()을 통해 값을 받아온다. 좋은 방법은 아니지만 테스트 코드이기 때문에 사용하겠다.

true가 나옴으로써 정상 작동임을 알 수 있다.

 


⛅️ Assertions를 이용하여 두 객체가 같은지 확인

위에서 member와 result 객체가 같은지의 여부를 System.out.println을 통해 console 창에서 확인할 수 있었다.

하지만 테스트 코드의 수가 많아지면 console 창을 통해 계속 확인하기 힘들어 질 것이다. 

이럴 때 assert라는 기능을 사용하면 유용하다.

 

assert란?

JUnit에서 테스트에 넣을 수 있는 정적 메서드 호출이다.

어떤 조건이 참인지 검증하며 테스트 케이스 수행 결과를 판별하는 역할을 한다.

JUnit에서 크게 두 가지 Assert 스타일을 제공하는데, 첫 번째로 전통적인 스타일의 Assert는 junit의 원래 버전에 포함되어 있고, 두 번째로 hamcrest 버전의 assert는 junit 4.4부터 추가되었으며 전자에 비해 여러 장점이 있다.

 

 

assertEquals() 이용

참고 링크: https://beomseok95.tistory.com/205

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

    repository.save(member);

    Member result = repository.findById(member.getId()).get();
    Assertions.assertEquals(member, result);
}

assertEquals(expected, actual) 의 형태로써, 객체 expected가 객체 actual와 같은지 확인한다.

만일 두 객체가 같다면 테스트 통과를 하게 된다.

 

 

assertThat() 이용

참고 링크 : https://jongmin92.github.io/2020/03/31/Java/use-assertthat/

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

assertThat(actual).isEqualTo(expected) 의 형태로써, assertEquals와 같이 두 객체가 같은지 확인하고, 같다면 테스트 통과를 한다.

 

 

junit에서 두 객체가 같은지 확인하는 방법은 assertEquals와 assertThat이 있는데, 전자가 junit의 원래 버전에 포함되어 있는 assert이고, 후자가 hamcrest 버전의 assert이다.

최근에는 hamcrest 버전의 assert를 사용하는 것을 선호하는데, 가독성, Failure 메세지, Type 안정성 등 여러 부분에서 기존의 assert보다 나은 점들이 있기 때문이다.

 

가독성을 예로 들어보면, assertEquals(expected, actual)은 expected와 actual의 위치가 헷갈릴 때가 있다. (김영한님도 헷갈려하셨다!)

하지만 assertThat(actual).isEqualsTo(expected)는 영어 해석하듯이 넣으면 되기 때문에 assertEquals에 비해 expected와 actual이 들어갈 자리가 좀 더 명확히 보인다. 

 


⛅️ findByName 기능 테스트하기

이번에는 findByName 기능을 테스트해보자. 

제대로 작동하는지 확인하기 위해

① member1, member2 객체를 두 개 생성하고 이름을 setName을 통해 각각 spring1, spring2로 설정한다.

② findByName("spring1")을 통해 name이 spring1으로 설정된 member 객체를 받아서 result 객체에 따로 저장한다.

 

이후 Assertions.assertThat()을 통해 result와 member1과 같다면 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();

    Assertions.assertThat(result).isEqualTo(member1);
}

 

테스트 성공

 

 


⛅️ findAll 기능 테스트하기

이번에 테스트할 기능은 findAll이다. findAll은 여태까지 저장한 모든 member들을 List 형식으로 반환하는 기능이다.

① member1, member2 이렇게 Member 객체를 두 개 생성하고, 이름을 각각 spring1, spring2로 설정한다.

② 그리고 repository.save를 통해 메모리 저장소에 두 객체를 저장한다.

③ findAll은 List형식으로 반환하기 때문에 List<Member> result를 선언하고 변수 result에 findAll의 결과 값을 넣는다.

④ Assertions.assertThat을 이용해 result의 size가 2인지 확인한다.

    만일 size가 2라면 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();
    Assertions.assertThat(result.size()).isEqualTo(2);
}

 

findAll 테스트 성공

 

 


⛅️ 테스트 코드 작성 시 유의할 점

테스트 코드의 좋은 점여러 개의 코드를 동시에 돌릴 수 있다는 점이다.

밑의 사진처럼 클래스 레벨에서 실행 버튼을 누르면 해당 클래스 내에 있는 메서드들이 실행된다.

클래스에서 모든 test 코드 실행

만일 Test 클래스가 여러 개라면 package에서 오른클릭을 한 후, 밑의 사진처럼 Run 옵션을 누르면 된다.

package 오른클 후 테스트 코드 실행

 

 

하지만 동시에 유의할 점도 있다.

위에 적은 방법 중 하나로 테스트 코드를 실행하게 되면 밑과 같이 findByName에서 오류가 나게 된다.

 

우선 이 오류가 발생한 원인을 알아보자.

위 오류 문구를 보면 Expected는 Member@1ea9f6af 이지만 Actual은 Member@188715b5로 두 객체가 달라 오류가 났다는 것을 알 수 있다.

 

왜 이런 상황이 발생했을까?

위 사진을 보면 테스트 함수의 실행 순서가 findAll -> findByName -> save 순서로 실행이 된 것을 볼 수 있다.

우리가 먼저 findAll에서 두 개의 객체를 생성하고 이름을 각각 spring1, spring2로 설정하고 repository에 저장했다.

findByName에서도 findAll에서와 같이 member 객체 두 개 생성하고 이름을 spring1, spring2로 설정했다. 그리고 이름이 spring1인 객체를 꺼냈다.

 

하지만 우리가 findAll에서 이름이 spring1인 객체를 먼저 넣었기 때문에, 해당 객체가 아직 repository에 남아있으므로 findByName에서 생성한 객체와 당연히 다를 수 밖에 없다.

 

해결하려면 어떻게 해야 할까?

모든 테스트는 순서와 상관없이 메서드 별로 다 따로 동작하도록 설계해야 한다. (순서에 의존적이게 설계하면 안된다.)

따라서 각 테스트 메서드가 실행된 이후에 repository에 담겨있는 데이터들을 clear 해주어야 한다.

 

Test 클래스에 테스트 메서드가 하나씩 끝날 때마다 repository를 비워주는 기능을 추가해주자.

repository를 비운다는 것은 MemoryMemberRepository에 있는 store 객체를 비워준다는 뜻이다.

하지만 store는 private로 설정되어 있기 때문에 MemoryMemberRepository 클래스 내에 store를 비워주는 코드를 작성한다.

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

 

그리고 MemoryMemberRepositoryTest 클래스@AfterEach 어노테이션을 이용하여 메소드가 하나씩 끝날 때 마다 clearStore가 실행되는 코드를 적는다.

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

 

이 후 전체 테스트 코드를 한 번에 실행하면 오류없이 잘 돌아가는 것을 볼 수 있다.

 

 

 

728x90