고딩왕 코범석

JUnit과 계층별 단위 테스트 정리 본문

Language & Framework/Spring

JUnit과 계층별 단위 테스트 정리

고딩왕 코범석 2021. 1. 31. 20:32
반응형

최근 스프링 프로젝트 공식 예제인 펫클리닉을 클론코딩 해보았습니다. 그리고 펫클리닉 기능들의 테스트 로직을 작성하며 얻었던 지식을 정리해보았습니다.


저는 국비지원 교육과정에서 프로젝트를 두번 진행한 경험이 있는데, 그 때 마다 테스트는 작성하지 않고 서버를 띄워 값이 정확히 들어왔는지 확인하는 방식으로 진행했습니다. 이후, 혼자 공부하면서 테스트에 대한 중요성을 알게 되었고, 펫클리닉을 기반으로 테스트를 어떻게 작성해야 하는지, 계층별로 확인해야 할 것들은 무엇인지 정리해보았습니다. 만약 포스팅에 틀린 설명이 있다면 꼭 피드백해주시면 고맙겠습니다.

JUnit

먼저 Java는 JUnit으로 테스트를 작성합니다. 프로젝트의 Test class들을 그대로 남김으로써 추후 개발자에게 테스트 방법 및 클래스의 history를 넘겨줄 수 있고, 어떤 로직이 수행되어야 하는지 볼 수 있어 매우 중요합니다.


다음으로, JUnit의 기본 구조를 살펴보면 아래의 그림 구조로 되어있습니다.

image

  • JUnit Platform : 테스트를 실행해주는 런쳐를 제공하며, TestEngine API를 제공합니다.
  • Jupiter : TestEngine API 구현체로 JUnit 5를 제공합니다.
  • Vintage : JUnit4, 3을 지원하는 TestEngine 구현체입니다.

이제 프로젝트에 있는 test 폴더를 보게 되면

image

java 프로젝트의 기본구조를 그대로 따르며, 인텔리제이를 쓸 경우 클래스에서 crtl + shift + T를 누르면 해당 클래스의 테스트 클래스를 바로 작성할 수 있습니다.

image

image

요롬코롬 클래스 이름도 클래스명 + "Test"라고 자동 작성되어있고, 테스팅 라이브러리를 선택할 수 있는데, 저의 경우는 JUnit5를 사용했습니다.

이후, 테스트를 작성하는데, 제가 기본적으로 썼던 어노테이션들에 대해 간략하게 짚고 넘어가겠습니다.

@Test

메서드 레벨에 작성하고, 해당 메서드를 테스트하겠다는 뜻입니다. 해당 메서드를 독립적으로 테스트하며, 인텔리제이 기준으로 메서드 옆에 run testmethod(ctrl + shift + F10)를 누르게 되면 해당 메서드만 테스트하고, 클래스를 run(ctrl + shift + F10)할 경우 해당 클래스 내부에 있는 모든 @Test가 달린 테스트들을 실행합니다.

@BeforeEach, AfterEach

BeforeEach, AfterEach는 각각 @Test메서드가 실행되기 전, 실행되고 나서 실행하는 메서드입니다.

@BeforeAll, AfterAll

현재 클래스에서 모든 @Test를 실행되기 전, 실행되고 나서 실행됩니다. Each와 All에서 알 수 있듯 만약 클래스 전체를 테스트 run할 경우 XXXEach는 각 테스트가 한번 실행될 때 마다 작동하며, XXXAll은 클래스를 실행하기 전혹은 후 딱 한번만 작동합니다. 이 때, 주의해야할 사항은 XXXAll을 사용시에 메서드는 static이어야 합니다.

Assertions Method

제가 주로 사용했던 메서드들 위주로 적어보겠습니다. 우선 검증하기 위한 메서드들은

org.junit.jupiter.api.Assertions에 위치합니다. 위에서 설명드렸듯, jupiter가 TestEngine의 구현체 이기에 여기에 검증하는 메서드들이 존재합니다. 이 외에도 많지만, 해당 위치에 속한 메서드들을 적어보자면

assertNotNull(a), assertNull(a) : a가 Null인지 아닌지 판별

assertEquals(a, b) : a, b가 같은 값인지 판별

assertAll() : 람다표현식를 이용하여 해당 매개변수란에 적혀있는 Executable형태의 모든 메서드들을 실행합니다. 단순히 assert문을 여럿 작성할 수 있는데, 차이점이 있다면 assertAll()을 사용시에는 안에 있는 Executable메서드들을 모두 실행하여 어떤 메서드가 검증해 실패했는지 모두 볼 수 있습니다. 이와 반대로 예를들어 assert문을 3개 작성했다고 가정했을 때, 2번째 assert문에서 검증이 실패하였다면, 3번째 메서드는 실행되지 않습니다.

이 외에 다양한 메서드들도 많고, 저도 다 써보지는 않아서 아는 메서드들만 정리해보았습니다. 아마 ide에서 찾아보시면 기본적인 메서드들은 어떤 역할을 하는지 파악이 쉽게 되실겁니다.


이제 계층별 단위 테스트에서 작성해보도록 하겠습니다.

Repository

저는 Jpa를 사용해서, @DataJpaTest를 사용했습니다. 이 어노테이션의 역할은 Jpa를 테스트할 수 있게 도와주며, 기본적으로는 인메모리 데이터베이스가 존재해야 합니다. 또한 Jpa에 필요한 클래스들만 로딩이 되어 좀 더 빠르게 테스트를 할 수 있는 장점이 있습니다.

image

이렇게 선언하며 @AutoConfigureTestDatabase 어노테이션의 replace값을 NONE으로 부여하여 실제 데이터베이스에 동작하도록 했습니다. 저는 인메모리 DB인 H2가 아닌 MySql을 사용했기 때문에 인메모리 데이터베이스가 아니라면 저 어노테이션을 설정해줘야 합니다.

image

Owner 등록(회원가입) 테스트입니다. 저는 given, when, then 형식을 사용했습니다.

  • given : 해당 메서드를 동작하기위해 주어지는 데이터
  • when : 해당 메서드 기능 동작
  • then : assert문을 통한 검증

Owner를 등록할 시, id값이 생겨야합니다. when절에 위치한 메서드 동작 후 해당 save된 객체의 id값이 null인지 검증했습니다. 결과는

image

저는 쿼리가 보이도록 설정했기 때문에 이렇게 쿼리가 나왔습니다. 테스트도 통과했네요.

요약해보자면 Repository 계층에서 테스트해야할 것은 해당 데이터의 CRUD가 잘 동작하는지 확인해야합니다. 기본적인 CrudRepository에 있는 기본 메서드 이외에 직접 작성한 쿼리들이 잘 동작하는지 확인해야 하는 계층입니다.

Service

Service 계층에는 비즈니스 로직들이 담겨있습니다. 이 비즈니스 로직들이 잘 동작하는지 테스트해야합니다. 하지만 의존성이 있는 객체들이 잘 동작하는지는 이 영역에서는 관심 밖입니다. 그래서 의존 객체들을 Mockito 프레임워크를 활용해 Mocking하여 테스트를 진행합니다.

image

@ExtendWith

Mockito 프레임워크를 사용하기 위해 해당 서비스 테스트 클래스 레벨에 사진 처럼 작성해줍니다.

@Mock

OwnerService 는 OwnerRepository를 의존하고 있습니다. 이 OwnerRepository 객체를 Mock으로 선언하겠다는 의미입니다. 만약 Repository 계층이 아직 구현이 되지 않았을 때, Mock으로 선언하여 stubbing으로 해당 테스트에서 의존 객체가 무엇을 해야하는지 stubbing 해줘야하는데, 이 설명은 뒤에서 설명드리겠습니다.

@InjectMocks

Mock으로 선언한 의존 객체들을 주입하는 어노테이션입니다. 테스트 진행시 ownerService에 있는 ownerRepository에 Mocking된 객체가 주입됩니다.


stubbing은 사진을 먼저 보여드리고 설명드리겠습니다. 제가 처음에 작성했을 때, 많이 애먹었던 부분이기도 합니다.

image

우선, Repository 객체가 Mock이기 때문에 우리가 해당 테스트 메서드에서 repository가 어떤일을 해야하는지 직접 설정해줘야 합니다. 다른 분들은 given..willReturn 등등 다양한 방식들로 쓰셨지만 저는 이 Mockito.when, then을 사용했습니다.

  • when : 해당 Mock객체의 행동을 설정합니다. 앞의 repository 계층에서 owner를 등록할 경우에는 save메서드를 사용하는데, 이 때 when절의 매개변수에 repository.save() 를 집어넣어 줍니다.
  • any : Mock객체의 메서드의 매개변수를 any타입으로 줄 수 있습니다. save의 메서드에는 owner 엔티티가 와야하는데, 테스트 계층에서 특정 값을 설정하기 보다는 모든 Owner 엔티티 클래스 형태의 값이 오면? 이라고 행동을 설정하는 겁니다.
  • thenReturn : 해당 when절의 메서드 실행시, 어떤 값을 리턴해야하는지 정해줍니다. 이 설정을 해주지 않으면, void 메서드는 아무일도 일어나지 않으며, Primitive는 기본 primitive(int는 0), Collection일 경우는 빈 Collection, 그 외에는 Null을 리턴합니다. 저의 경우는 위에 미리 정의한 owner를 리턴하도록 설정했습니다.
  • verify : stubbing된 메서드의 호출 여부를 검사합니다. 매개변수 첫번째는 mock객체, 두번째는 빈도수를 나타내며 두번째 매개변수를 적지 않을 경우에는 1이 디폴트입니다. 그 뒤에 우리가 호출했는지 검사하려는 메서드를 작성해주시면 됩니다.

Controller

Controller에서는 해당 메서드 호출시 응답, 반환시 형태에 맞게 값이 잘 반환 되었는지, 요청 시 파라미터에 대한 검증 등등을 확인해야 합니다. 이 외에 Controller에서 검증애햐 할 부분들이 많은데, 저는 RestController로 클론코딩을 진행했기 때문에, 간단히 세 가지의 경우에 대해 말씀드려볼까 합니다.

image

해당 클래스 위에 @WebMvcTest 작성하고, 테스트할 컨트롤러 클래스를 집어넣습니다. 이렇게 되면 MockMvc를 자동주입받아 사용할 수 있습니다. ObjectMapper는 request dto를 json으로 파싱하기 위해 작성하였습니다.

Mock과 MockBean의 차이?

Mock은 우선 mockito 프레임워크에서 제공하는 어노테이션 입니다. org.mockito에 위치해 있으며 MockBean은 스프링 테스트에서 제공하는 어노테이션 입니다.org.springframework.boot.test.mock.mockito 에 위치하며, MockBean을 사용하면 Bean이 스프링 컨테이너에 존재해야 할 경우는 해당 객체가 Mock으로 주입됩니다. 사용법은 둘 다 기존의 방식대로 사용하면 되고, 어떤 경우에 둘중 무엇을 써야하는지는 아직까지 파악하질 못해서 WebMvcTest에서는 MockBean, Service 계층에서는 Mock, InjectMocks를 활용했습니다.


이제 owner를 등록하는 메서드를 컨트롤러에서 어떻게 테스트 작성을 해야하는지 사진을 보겠습니다.

image

확실히 깁니다..ㅠ 우선 컨트롤러 계층에서는 ownerService를 의존하고 있기에 ownerService를 stubbing 해주었습니다. 저는 owner등록 후, 등록된 owner id로 owner의 정보를 responseDto에 넣어 json으로 응답해주었는데요. mock객체를 stubbing 해주는 것은 앞에서 다뤄 봤기에 when, then을 살펴보겠습니다.

  • perform : 어떤 요청인지, 요청에서 바디에 담긴 타입과 내용들은 무엇인지, 요청에 대한 정보들을 설정합니다. 이 메서드의 매개변수에는 RequestBuilder 타입이 들어올 수 있습니다. 사진의 post().contentType().content() 의 반환 타입은 RequestBuilder를 구현한 MockHttpServletRequestBuilder 타입으로 반환됩니다.

  • post : 이외에 put, get등등 여러 api 방식이 있습니다. 매개변수에는 요청 uri를 작성하면 됩니다.

  • contentType : 바디에 담길 내용의 타입을 지정해줍니다. 저는 json으로 보냈습니다.

  • content : json으로 타입을 지정해주었기에, requestDto를 objectMapper를 이용해 json으로 변환했습니다.

  • andExpect : 요청을 보낸 후 응답에 대해 검증하는 메서드입니다. ResultMatcher 타입의 매개변수가 와야합니다.

  • status().isOk() : 응답이 성공적으로 되었는지 체크하는 메서드 입니다.

  • content().contentType() : 해당 바디 타입이 어떤 형태인지 체크합니다. 저는 json으로 반환되길 원하기에 json으로 설정했습니다.

  • jsonPath : 해당 json의 값이 올바르게 나왔는지 체크하는 메서드 입니다. 표현식을 통해 해당 json의 key를 설정하고, org.harmcrest 안의 is()를 이용해 해당 key와 value가 맞는지 비교해줍니다.

이렇게 제가 클론코딩에서 진행했던 테스트들을 설명드렸습니다. 테스트를 작성하는 방법은 너무 다양하기에 이 글에만 의존하지 마시고 꼭 좀 더 찾아보길 권해드립니다. 저도 테스트에 대해 공부해야 할 부분이 많기에 경험했던 부분들만 나름대로 축약하여 작성했습니다. 부족한 것이 있다면 꼭 피드백 부탁드립니다!

반응형

'Language & Framework > Spring' 카테고리의 다른 글

@Async, 비동기 기능  (0) 2021.02.16
ResponseEntity란?  (4) 2021.02.08
트랜잭션에 대해 알아보자.  (0) 2020.11.28
의존관계 주입(Dependency Injection)  (0) 2020.11.19
DTO란?  (0) 2020.10.30