본문 바로가기

Spring/스프링 입문

02. 회원 관리 페이지 만들기 - (3) 회원 리포지토리 테스트 케이스 작성

💡 본 게시글은 김영한님의 인프런(Inflearn) 강의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술에 대해 공부하고, 정리한 내용입니다.

1) JUnit을 이용한 테스트 코드 작성

이번에는 이전에 구현한 기능들이 제대로 작동하는지 확인하기 위한 테스트를 진행하겠습니다. 테스트를 실행하는 방법을 생각해보면, 즉시 두 가지 방법이 떠오릅니다.

  1. Java의 main 메서드를 통해 실행
  2. 웹 애플리케이션의 컨트롤러를 통해 해당 기능을 실행

 

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

 

Java에서 제공하는 'JUnit' 프레임워크를 이용해 테스트 코드를 작성하면 이러한 단점들을 해결할 수 있습니다. 그럼 지금부터 JUnit 프레임워크를 사용하여 테스트 코드를 작성해보도록 하겠습니다.


2) 기능을 테스트할 Class 만들기 

먼저 테스트 코드를 작성할 Class를 생성해야 합니다. 

 

이번에 테스트 해볼 기능은 'MemoryMemberRepository'의 기능들(save, findById, findByName, findAll)입니다. 보통 테스트 코드를 작성할 때, 일반적으로 테스트 대상의 이름을 그대로 따르고, Class의 경우 뒤에 'Test'를 붙이는 것이 일반적입니다.

 

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

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

 

 위와 같이 'MemoryMemberRepository'의 기능들을 확인할 'MemoryMemberRepository' 객체를 하나 생성해줍니다. 이제 이 객체를 이용하여 테스트를 진행하면 됩니다.


3) save 기능 테스트하기

'MemoryMemberRepository'에서 구현한 'save' 기능이 제대로 작동하는지 확인하는 테스트 코드를 작성하겠습니다. 

 

(1) 우선 함수를 선언하고, 위에 '@Test'를 표시합니다. 

@Test
public void save(){
}

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

 

(2) 이제 해당 메소드가 저장이 잘 되는지 확인하기 위해서 다음과 같이 코드를 작성하고, 밑의 과정을 거칩니다.

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

① 'Member' 객체를 생성하고 'setName'을 통해 이름을 'spring'으로 지정합니다.

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

③ 저장이 됐는지 확인하기 위해 'member.getId()'에 해당하는 'member'객체를 'findById'를 통해 'repository'에서 찾아옵니다. 그리고 'Member'형 객체 'result'에 저장합니다. (시스템이 저장한 ID를 통해 다시 해당 ID로 설정된 'member' 객체를 찾아옵니다.)

④ 'member'와 'result'가 같다면 기능이 정상 작동한다는 것을 알 수 있습니다. 두 객체가 같은지의 여부를 콘솔 창에 출력합니다. 'true'라면 두 객체가 같다는 것을 의미합니다.

 

 'findById'를 구현할 때 'Optional'을 반환하도록 설정했습니다. 이 테스트 코드에서 'member' 객체를 받아서 비교하기 때문에 '.get()'을 통해 값을 받아옵니다.

 

좋은 방법은 아니지만 테스트 코드이기 때문에 사용합니다. 'console' 창에 'true'가 출력되면, 'save' 기능이 정상적으로 작동한다는 것을 알 수 있습니다.


4) Assertions를 이용하여 두 객체가 같은지 확인

 위에서는 member와 result 객체가 같은지의 여부를 System.out.println을 통해 console 창에서 확인할 수 있었습니다. 하지만 테스트 코드의 수가 많아지면 console 창을 통해 계속 확인하기 힘들어 질 것입니다. 이럴 때는 JUnit의 'assert' 기능을 사용하면 매우 유용합니다.

 

'assert'JUnit에서 테스트에 넣을 수 있는 정적 메서드 호출로, 어떤 조건이 참인지 검증하며 테스트 케이스 수행 결과를 판별하는 역할을 합니다. JUnit에서는 전통적인 스타일의 Assert와 hamcrest 버전의 Assert 두 가지 스타일을 제공합니다.

 

(1) 'assertEquals()'를 이용한 방법은 다음과 같습니다.

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'와 같은지 확인합니다. 두 객체가 같다면 테스트 통과를 하게 됩니다.

 

(2) '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보다 우수하기 때문입니다.


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

 

 위의 코드를 보면, 'spring1'이라는 이름을 가진 'member1' 객체를 저장하고, 이를 'findByName()' 메서드를 통해 찾아 'result' 객체에 저장합니다. 그리고 이 'result' 객체가 'member1' 객체와 같은지를 'Assertions.assertThat().isEqualTo()'를 통해 확인합니다.

 

 이때 'result'와 'member1'이 같다면, 'findByName' 기능이 정상적으로 작동하는 것으로 판단하게 됩니다.


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

7) 테스트 코드 작성 시 유의할 점 (중요.)

테스트 코드의 장점 중 하나는 여러 개의 코드를 동시에 실행할 수 있다는 점입니다. 클래스 레벨에서 실행 버튼을 누르면 해당 클래스 내에 있는 모든 메서드들이 실행됩니다.

 

만약 Test 클래스가 여러 개라면, 패키지에서 오른 클릭 후 Run 옵션을 선택하여 모든 테스트 코드를 한 번에 실행할 수 있습니다.

 

그러나 동시에 주의해야 할 점이 있습니다. 만약 테스트 코드를 위와 같은 방법으로 실행하면, 테스트 코드의 순서에 따라 오류가 발생할 수 있습니다.

 

예를 들어, 'findAll' 테스트 후 'findByName' 테스트를 실행하면, 'findByName'에서 오류가 발생할 수 있습니다. 이는 'findAll'에서 생성한 객체가 'findByName'에서 생성한 객체와 달라서 발생하는 문제입니다.

 

이 문제를 해결하기 위해서는 모든 테스트가 순서와 상관없이 독립적으로 동작하도록 설계해야 합니다. 따라서, 각 테스트 메서드가 실행된 이후에 저장소(repository)에 담겨 있는 데이터들을 초기화해야 합니다.

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

 

위와 같이 'MemoryMemberRepository' 클래스 내부에 'store' 객체를 비워주는 'clearStore()' 메서드를 추가할 수 있습니다. 그리고 'MemoryMemberRepositoryTest' 클래스에 '@AfterEach' 어노테이션을 사용하여 각 테스트 메서드가 실행된 후에 'clearStore()' 메서드가 실행되도록 설정할 수 있습니다.

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

 

이렇게 설정하면, 모든 테스트 코드를 한 번에 실행해도 각 테스트는 독립적으로 동작하며, 이전 테스트의 결과에 영향을 받지 않습니다. 이를 통해 테스트의 신뢰성을 높일 수 있습니다.