[Spring boot] Spring boot test starter를 이용한 테스트 코드 작성

지난 포스트에서 잠깐, 테스트 코드에 대해 다뤄봤었습니다. 테스트 코드를 이용하면, 확실히 Postman이나 Curl과 같은 테스트 도구없이도 자동화 된 테스트 프레임워크를 이용해서 쉽게 테스트를 할 수 있었죠.

실제로 테스트 프레임워크를 이용한 테스트 코드 구현은 현업에서 많이 이용합니다. 단순하게 내가 구현한 기능 한 단위만 테스트를 하는 것이 아닌 종합적으로 테스트할 때는 이러한 자동화가 필요합니다. 특히 프로젝트가 커지고, 그 규모가 엔터프라이즈급이 되면 애플리케이션 코드도 방대해지고, 그렇게 되면 테스트해야 할 함수도 많아지겠죠.

 

Spring boot Test

Spring boot에서는 기본적인 테스트 스타터를 제공하는데, 이 모듈에는 Junit 뿐만 아니라 왠만한 Java 계열의 언어에서 사용하는 테스트 라이브러리들이 한대 모여 있습니다. 

  • spring-boot-test
  • spring-boot-test-autoconfigure

이 두 가지 모듈이 하나로 되어 있는 것이 바로 spring-boot-starter-test입니다. 테스트 환경에 대한 자동 설정 파일은 테스트 코드를 구현하는 데 있어, 까다롭게 설정해야 하는 값들을 건드리지 않아도 기본적인 테스트는 할 수 있도록 한 것이 메리트죠.

기본적으로 알고리즘 코드를 짜고, 하는 테스트를 유닛 테스트, 비즈니스 로직 하나를 구현하고 짜는 테스트 역시 유닛 테스트라고 합니다. 하지만 웹 애플리케이션에서는 유닛 테스트 또한 중요하지만, UI Test, App Test, Data Test, JsonTest 등 다양하게 테스트 범위가 넓혀져 있다는 것이 포인트이고, 실제 테스트를 할 때는 이에 대한 테스트 코드를 별도로 구성해야 한다는 것입니다.

Spring boot starter test는 이러한 테스트를 쉽게 구현할 수 있도록 다음과 같은 다양한 어노테이션을 제공합니다.

  • @SpringBootTest
  • @WebMvcTest
  • @DataJpaTest
  • @RestClientTest
  • @JsonTest

이 외에도 Spring boot 2.x에서 추가된 새로운 어노테이션도 존재합니다.

  • @WebFluxTest
  • @JooqTest
  • @DataLdapTest
  • @DataNeo4jTest
  • @DataRedisTest

이 포스트에서 여기에 있는 어노테이션을 전부 다루기는 어렵고, 몇 가지 다뤄보면서 어떤식으로 테스트가 이루어지는지를 중점적으로 써나가보도록 하겠습니다.

 

@SpringBootTest

SpringBootTest 어노테이션은 애플리케이션 레벨의 테스트입니다. 애플리케이션 레벨의 테스트란, 서버 프로그램의 환경 설정이나 포트 주소 등 실행될 때의 런타임 테스트를 말합니다.

특히 여러 단위 테스트를 하나의 통합된 테스트로 수행할 때 적합한데, Spring boot 1.4 버전부터 제공되고, 우리가 지난 포스트에서도 같이 사용한 어노테이션입니다. 이 어노테이션을 수행하면, 직접 서버 애플리케이션이 자동으로 올라가게 됩니다.

이 어노테이션의 작동 원리는 실제 구동되는 애플리케이션과 똑같이 Application Context를 로드하여 테스트하게 됩니다. 안드로이드로 보자면, Instrument Test라고 해서, 직접 VM에 기기를 올리고 하는 테스트이기 때문에 테스트 속도가 엄청 느린 반면, 모든 테스트를 할 수 있죠. 따라서 애플리케이션의 종합적인 테스트를 하고자 할 때 적합합니다.

그럼 간단하게 테스트 코드를 작성해보도록 하죠.

어노테이션에 대한 파라미터를 각각 설명드리자면...

  • value: 테스트가 실행되기 전에 적용할 Property로, 기존의 Property를 Override 합니다.
  • properties: 테스트가 실행되기 전에, {key=value} 형식으로 Property를 추가 합니다.
  • classes: Application Context에 로드할 클래스를 지정합니다. 별도로 지정하지 않을 경우, @SpringBootConfiguration을 찾아서 로드하게 됩니다.
  • webEnvironment: 애플리케이션이 실행될 떄의 웹 환경으로, 기본값은 기본 설정된 Mock Servlet을 로드합니다. 거기에 추가로, 위 코드에서는 포트 주소를 랜덤으로 한 것입니다.

위 코드를 실행하면, 실제로는 실행되지 않습니다. 어떤 오류가 나타나냐면, value와 properties를 같이 사용할 수 없다는 오류가 나타나게 됩니다.

두 파라미터가 다른 점은 아래와 같습니다.

value는 기존의 properties 업데이트하는 반면, properties는 새로 추가된다는 속성을 지니고 있습니다. 따라서 두 개의 파라미터에 같은 값을 넣는 것은 안됩니다.

그런데, 이러한 환경 변수 설정은 지난 포스트에서 YAML 파일을 통해 별도로 설정하는 방법을 이용할 수 있었는데요. 어차피 그런 방법을 사용하면 될텐데, 굳이 이렇게 별도로 주어주게 되면, 오히려 한 코드에서 가독성만 나빠지겠죠?

그럴 때는 YAML 파일을 하나 구성해놓고, @ActiveProfiles 어노테이션을 이용해주면 됩니다.

어노테이션에 적용할 프로파일 이름을 넣어주면 끝.

실행해보면, test 환경으로 실행되었음을 알 수 있고, 포트 주소는 랜덤된 번호로 부여되고 있음을 알 수 있습니다.

이 외에도, 팁이 있다면, 테스트 환경에서 테스트를 수행할 경우, 특히 DB의 경우 @Transactional이 적용된 코드에 대해서는 오직 테스트만 수행하는 것이기 때문에 롤백이 됩니다.

그러나, 테스트가 서버의 다른 스레드에서 실행 중일 경우에는 RANDOM_PORT나 DEFINED_PORT를 사용하여 테스트를 수행해도 트랜잭션이 롤백되지 않기 때문에, CI/CD 환경 구성에 참고 하시는 게 좋겠습니다.

또 한가지는 @SpringBootTest 어노테이션에는 기본적으로 Spring boot Application 클래스를 찾는 검색 알고리즘이 적용되어 있는데, 위에서 설명했다시피 @SpringBootApplication 혹은 @SpringBootConfiguration 어노테이션을 찾기 때문에 반드시 애플리케이션에 이러한 어노테이션을 명시해주도록 합시다.

 

@WebMvcTest

이름 그대로 MVC를 위한 테스트입니다. 대표적으로 웹에서 테스트하기 어려운 Controller 테스트들이 있습니다. 예를 들자면, Form에 여러가지 데이터를 한 번에 입력하고, 테스트 했는데, 다시 또 테스트를 반복해야 한다면? 다시 값을 입력해야 하고, 손이 많이 가죠.

웹 상에서 Request, Response에 대해 테스트할 수 있는 어노테이션이고, 심지어 Spring Security에서 제공하는 로그인, 로그아웃, 세션, 필터까지 자동으로 테스트할 수 있기 때문에 매우 유용합니다.

위에서 다뤄본 @SpringBootTest 어노테이션에서도 이러한 테스트가 가능하지만, @WebMvcTest는 애플리케이션 컨텍스트를 통해 서버 앱을 전부 로드하는 것이 아니라 MVC 관련 어노테이션인 @Controller, @ControllerAdvice, @JsonComponent와 Filter, WebConfigurer, HandlerMethodArgumentResolver만 로드되기 때문에 가볍게 테스트할 수 있다는 장점이 있습니다.

간단하게 테스트해보기 위해 POJO 객체와 이를 수행하는 Controller를 하나 만들어보도록 하죠.

Kotlin에서 제공하는 data class를 이용해, 상품 번호, 상품 이름, 상품 가격, 상품 입고 날짜를 가진 필드의 POJO 객체를 만들어보도록 하겠습니다.

Controller 코드는 상품 리스트를 받아오는 코드입니다. 엔드포인트로 "/books"라는 포인트에 GET 메소드 요청시, BookService 클래스에 상품 목록을 요청하여, "itemList"라는 키 값으로 데이터값을 넘기는 Controller입니다. Controller에서 반환되는 View 이름은 "item"으로 지정하였습니다.

다음과 같이 인터페이스를 생성해줍니다. 인터페이스는 Item 타입의 리스트를 넘기는 메소드 하나만을 만들었습니다. 실제로 이 인터페이스는를 구현하는 구현체는 만들지 않겠습니다. (단순 테스트만 진행할 것이기 때문에..) 구현체가 없는 대신 Mock 데이터를 이용해서 테스트를 진행하도록 하겠습니다.

@WebMvcTest를 사용하기 위해서 먼저 해야할 일은 테스트할 특정 Controller의 클래스 이름을 명시해줘야 합니다. 주입된 MockMvc는 Controller 테스트시, 모든 의존성을 로드하는 것이 아닌 ItemController 관련 Bean만을 로드하기 때문에 가벼운 MVC 테스트 수행이 이루어지며, 이 때는 전체 HTTP 서버를 실행하지 않습니다.

@Service 어노테이션이 적혀있는 ItemService는 실제 @WebMvcTest 적용 대상은 아닙니다. 다만 ItemService 인터페이스를 구현한 구현체 없이 가짜 객체로 대체해야만 Controller에서 해당 메소드를 이용할 수 있기 때문에 @MockBean을 활용하여 Controller 내부의 의존성 요소를 마치 진짜인 것처럼 속인 것입니다. 여기서 가짜 객체를 Mock Object라고 이야기 합니다. 실체 객체는 아니지만 특정 행위를 지정하여 실제 객체처럼 동작하게 만들 수 있는 요소입니다.

@MockBean을 사용할 경우, 객체는 가짜지만 행위는 진짜처럼 행동하기 때문에 실제로 getItemList()에서 item을 반환합니다. 이 때 테스트를 진행할 경우, given() 메소드를 사용하여 실제 리스트에 데이터를 넣어줘야합니다. 그래야만 최종적으로 이것이 반환되는지 테스트를 할 수 있겠죠?

여기까지 끝났으면, 마지막으로 MockMvc를 사용해 해당 URL의 상댓값, 반환값에 대한 테스트를 수행할 수 있습니다.

  • andExpect(status().isOk): HTTP 응답값이 200인지 테스트 (200은 성공을 의미합니다.)
  • andExpect(view().name("item")): 반환되는 뷰의 이름이 "item"인지 테스트
  • andExpect(model().attributeExists("itemList")): 모델의 property 중 "itemList"라는 property가 존재하는지 테스트
  • andExpect(model().attribute("itemList", contains(item))): 모델의 property 중 "itemList"라는 property에 item 객체가 담겨져 있는지 테스트

이 테스트는 Spring MVC일 때 유용하게 쓰일 수 있습니다.

 

@DataJpaTest

아직 JPA에 대해 다루진 않았지만, 간단히 설명드리자면, JPA는 Spring boot에서 DB와 연동하기 위한 모듈입니다. 과거에는 Batis 등을 이용해 SQL 질의를 직접 작성하는 경우가 많았지만 최근에는 ORM이 대세를 이루면서 JPA를 이용한 DB 연동이 더 많아지는 추세죠.

@DataJpaTest는 JPA 관련 테스트 설정만을 로드하는데, DataSource의 설정이 유효한지 혹은 정상인지, JPA를 이용해 DDL, DML이 정상적으로 수행되는지 등의 테스트가 가능합니다. 물론 테스트 하는 방법은 Docker, Kubernetes 등 다양한 환경을 이용할 수도 있겠지만, Spring boot에서는 H2라는 In-memory DB를 기본적으로 제공하기 때문에 이를 이용해도 괜찮습니다.

실제로 @ActiveProfiles는 이것 때문에 많이 사용됩니다. 운영 DB에서 버그를 수정하고, 하는 일은 있어서는 안되기 때문에 개발용 DB, 테스트용 DB를 별도로 구축하여 사용하는 편인데, 이들 DataSource 설정이 전부 다르니, 개발할 때 바꾸고 테스트할 때 바꿀 수가 없기 때문에 환경 설정을 프로퍼티를 통해 설정하는 것이죠.

먼저 테스트를 위해 Data class를 이용해서 Item에 대한 Entity 클래스를 만들어줍니다. Item 도메인 객체에 JPA 테스트를 수행할 수 있게 Item 클래스에 JPA 관련 어노테이션만을 추가하면 됩니다.

JPA 권장에 따라 Repository 패턴을 맞춰 ItemRepository 인터페이스도 같이 만들어줍니다.

이번에는 생성자 주입 방식으로 EntityManager와 Repository를 주입하였고, 테스트 코드는 총 3개로 구분하였습니다. Item이라는 랜덤의 상품을 생성하고, testEntityManager의 persist 메소드를 이용하면 정상 동작하는지를 테스트할 수 있습니다.

IsEmptyCollection은 Collection 자료구조가 비어 있는지를 테스트하는 클래스로 empty 메소드는 비어 있는 Collection 자료구조를 생성하게 되는데, 이와 일치하면 테스트를 통과시키도록 구현하였습니다.

모든 테스트는 H2 DB에서 이루어지며, 위에서 설명했던대로 테스트가 종료되면 모든 데이터는 롤백됩니다.

 

@RestClientTest

이전 포스트에서는 REST 관련 테스트를 위해 Curl이나 Postman을 소개하였습니다. 하지만 이들의 테스트는 단위 테스트에 불과하기 때문에 일일이 수동으로 전체적인 테스트를 진행하는 데는 한계가 있었습니다.

REST 통신에 관련된 테스트의 주는 대부분 JSON 형식에 맞춰 예상대로 반환하는지에 대한 테스트가 대부분입니다. 위의 테스트가 DB와 정상적으로 통신하고 제대로 트랜잭션이 되고 있는지에 대한 내부 테스트였다면, 이번 테스트는 클라이언트의 요청을 서버가 정상적으로 수행하는지에 대한 테스트라고 보시면 됩니다.

테스트를 위해서 간단한 API 한 개를 만들어보도록 하겠습니다.

먼저 RestService 클래스를 한 개 만들어보도록 하겠습니다. 이 클래스에서 사용하는 RestTemplate은 실제 URI에서 자원을 요청하는 객체로 TimeOut, ReadTimeOut 등 다양한 설정이 가능해서 테스트하기 용이한 클래스죠.

GET 방식으로 통신하기 위해서는 getForObject, POST 방식은 postForObject를 사용하면 되는데, 여기에서는 GET 방식으로 테스트를 진행해보도록 하겠습니다.

위에서 만들었던 RestService를 주입하고, GET 방식의 API를 만듭니다.

@RestClientTest 역시 테스트 대상이 되는 Bean을 주입 받습니다. MockRestServiceServer 클래스는 클라이언트와 서버 사이의 REST 테스트를 위한 객체입니다. 실제로 통신이 이루어지게끔 구성할 수도 있지만 위 코드에서는 실제로 통신까지는 이루어지지 않고, 지정한 경로에 예상되는 성공 결과, 실패 결과가 알맞게 떨어지는지만 간단히 보는 테스트 코드입니다.

andRespond 메소드를 이용하여 여러분들이 원하는 시나리오를 만들 수 있습니다. 위의 코드는 "/rest/test"로 요청을 보냈을 때, 현재 리소스 폴더에 생성되어 있는 test.json의 파일을 응답해주도록 시나리오 되어 있는 것입니다. 실제 Spring boot에서는 정적 경로의 있는 리소스를 정의할 수 있는데, 이의 최상위 경로가 바로 resources입니다.

이렇게 resources 폴더에 test.json 파일을 만들어주면 되는데, 내용은 아래와 같이 입력해주세요.

{
  "idx": null,
  "name": "테스트",
  "price": 0,
  "date": null
}

코드를 보면, test.json 파일을 갖온 다음, 그 값의 name 필드가 "테스트" 값인지를 확인하는 것이기 때문에 name 필드를 제외한 나머지는 자유롭게 입력하셔도 무방합니다.

같은 방법으로 andRespond 안에 응답 시나리오를 만들 수도 있습니다. withServerError 메소드를 이용하면 500 오류를 응답 시키는 시나리오를 만들어주는데, 이 때 오류를 핸들링 하기 위해서는 Junit 5에서 제공하는 assertThrows 메소드를 이용해줍니다.

assertThrows는 핸들링 할 오류와 함께 Lambda 함수식의 익명 함수를 넣어, 요청할 코드를 넣으면 되는데, 만약 요청한 코드가 없으면 오류가 발생하지 않은 것으로 간주되니, 반드시 API 요청 코드를 삽입하셔야 합니다.

 

@JsonTest

마지막으로 JsonTest를 보도록 하겠습니다. JsonTest는 위의 테스트와는 조금 차이가 있습니다. 위의 테스트는 Json의 응답을 받아 POJO 객체로 처리한 뒤, 해당 데이터의 정확성을 검증하는 테스트 코드였다면, 지금은 응답이 정확하게 JSON으로 직렬화 되어 전달되는지 확인하는 테스트입니다.

그래서 테스트 과정은 총 두 가지가 있습니다. 문자열로 나열 된, JSON 데이터를 객체로 변환하여 변환된 값을 테스트하는 경우와 그 반대의 경우이지요. 실제 Spring boot starter test에서는 이러한 직렬화 라이브러리를 별도로 사용하는데, 그 대표적인 라이브러리가 Jackson과 Google의 Gson 라이브러리가 있고, 실제로 Spring boot starter test에서는 두 라이브러리 모두 포함하고 있습니다.

Jackson 라이브러리를 이용해서 위에서 올린 test.json을 테스트 해보도록 하겠습니다. 먼저 POJO 객체를 하나 만들어서, name만 "테스트"라는 이름을 가진 것으로 만들고 나머지는 랜덤한 값을 집어넣습니다. 이 객체는 POJO 객체랑 역직렬화, 직렬화한 데이터와 일치하는지 보기 위한 테스트입니다.

문자열 형태로 역시 name만 "테스트"인 JSON 문자열을 만들어주시고, 이제 Jackson 라이브러리를 이용해 json 파일과 문자열을 직렬화, 역직렬화하여 값이 맞는지 테스트하면 됩니다.

 

마치며...

기본적으로 Spring boot 외에도 Django, Flask, Express 등 다양한 웹 프레임워크에서 이뤄질 수 있는 개발에 대한 테스트를 진행해봤습니다.

사실 Spring을 다루면서 좀 까다로운 점은 버전이 단기간에 바뀌면서 기능이나 함수, 코드들이 많이 바뀝니다. 실제로 Junit 4를 쓰시는 분들도 있고, 저처럼 버전을 빨리 바꿔타면서 Junit 5로 오신 분들도 있을 것입니다. 한 번 쯤 테스트 코드를 짜보신 분들은 아시겠지만 코드가 많이 다릅니다.

또, 최근에는 Rx라는 것이 등장하면서, 비동기 처리에 대한 테스트인 WebFlux 테스트도 있는데, 이 이후의 테스트는 각각 한 개씩 포스트를 써가면서 한 번 써보는 과정을 적어보도록 하겠습니다. 이번 포스트에서는 간단한 Spring boot 앱을 개발하면서 사용하는 REST, JSON, MVC 등 기본적인 것만 살펴봤습니다.

comments powered by Disqus

Tistory Comments 0