[Spring Boot] 7. 테스트(Testing)

참조 > https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing

테스트(Testing)

백문이 불여 일타.

다음과 같은 샘플 컨트롤러와 샘플 서비스가 있다.

SampleController.java
1
2
3
4
5
6
7
8
9
@RestController
public class SampleController {

private SampleService sampleService;
@GetMapping("/hello")
public String hello() {
return "hello"+sampleService.getName();
}
}
SampleService.java
1
2
3
4
5
6
Service
public class SampleService {
public String getName() {
return "GeunChang";
}
}

컨트롤러는 서비스의 getName()을 호출해서 문자열을 합하여 반환한다.
이에 대한 테스트는 어떻게 하면 좋을까?

1. 의존성 확인

스프링부트 테스트에서 가장 처음 할 일은 의존성 확인하고 없으면 추가하는 것이다.
spring-boot-testspring-boot-test-autoconfigure가 존재하나 늘 그렇듯이
spring-boot-starter-test한방이면 거의 모든 테스트가 커버 된다.

pom.xml
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

스프링부트는 이리 간단하다. 이거 하나만 해놓으면 많은게 들어온다.

  • JUnit
  • Spring Test & Spring Boot Test
  • AssertJ
  • Hamcrest
  • Mockito
  • JSONassert
  • JsonPath

2. 스프링 부트 테스트

스프링부트 어플리케이션은 결국 스프링 프레임워크의 ApplicationContext이므로
스프링 프로젝트에서 하던 테스트와 크게 차이는 없다.

1) 기초

정리, 정리

테스트러너

JUnit 프레임워크에서 테스트 클래스 내에 존재하는 각각의 메소드 실행을 담당하는
클래스며 테스트 클래스의 구조에 맞게 테스트 메소드들을 실행하고 결과를 표시하는
역할을 한다. 모든 테스트 러너는 org.junit.runner.Runner추상 클래스를 상속하고
있으며 별도의 테스트러너를 따로 지정하지 않는 경우 JUnit은 기본 테스트 러너인
BlockJUnit4ClassRunner를 호출한다.
이클립스나 인테리 J에서 테스트 결과가 이쁘게 나오는 것도 이 기본 테스트 러너의 결과를
IDE가 해석해서 화면으로 보여주는 것이다.

@RunWith, SpringJUnit4ClassRunner.class, @ContextConfiguration

@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션이다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일위치를 지정한 것이다. 스프링의 JUnit 확장기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다.

Toby LeeToby's Spring-Vol.1 p.185

–풀이–ㅋㅋ

기본 테스트러너를 사용하지 않고 다른 테스트러너를 사용할때 @RunWith어노테이션을
사용한다. JUnit에는 카테고리 테스트러너나 SuiteClasses등 여러 러너가 존재하며 개발자가 직접
org.junit.runner.Runner를 상속해서 커스텀 테스트 러너를 만들어 자신만의 고유한
기능을 추가해서 테스트에 사용하기도 하는데 이때도 @RunWith를 사용해 테스트러너를
지정한다.

스프링 프레임워크에서 SpringJUnit4ClassRunner라는 테스트 러너를 제공해주며 이를
@RunWith로 지정하면 @Repeat, @ProfileValueSourceConfiguration,
@ifProfileValue등 스프링에서 제공하는 추가적인 테스트 기능을 사용할 수 있다.

JUnit은 각각의 테스트가 서로 영향을 주지 않게 하기 위해 각 @Test마다 새로
오브젝트를 생성하는데 이떄문에 ApplicationContext도 매번 새로 생성되야해서
테스트가 많을 경우 속도가 많이 느려진다. SpringJUnit4ClassRunner 테스트 러너에는
위 토비의 스프링에서도 말했듯이 ApplicationContext의 생성/관리 기능이 추가되서
각각의 테스트별로 오브젝트가 생성 되더라도 싱글톤의 ApplicationContext를 보장함
으로써 속도를 보안한다.
@ContextConfiguration으로 테스트때 자동으로 관리할 ApplicationContext
메타 설정파일 위치를 지정한다.

1
@ContextConfiguration(locations="applicationContext.xml")

경로지정이 없다면 테스트 클래스 파일이 있는 패키지 내에서
다음과 같은 이름의 설정 파일을 사용하게 된다.

  • ContextConfigLocationTest-context.xml
  • contextconfiglocationtest-context.xml

spring-test 4.3 부터 SpringRunner가 사용된다.
소스를 까보면 SpringRunner는 SpringJUnit4ClassRunner를 상속(extend)하고 있다.

2) @SpringBootTest

참조 : https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing-spring-boot-applications

‘spring-boot-test’는 @SpringBootTest라는 어노테이션을 제공하는데 위에서 풀이한
spring-test의 ‘@ContextConfiguration’에 스프링부트의 확장이 더해진 것으로 볼 수 있다. 매우 다양한 기능을 제공하는 @SpringBootTest를 사용하는 입장에서 가장 중요한
것은 JUnit4 사용시 반드시@RunWith(SpringRunner.class)가 있어야 한다는 점이다. 그렇지 않으면 어노테이션들이 무시된다.

JUnit 5를 사용하면 ‘@SpringBootTest’에 따로 @ExtendWith(SpringExtension)등을
붙이지 않아도 된다. (‘@SpringBootTest’ 뿐 아니라 다른@..Test들에도 적용)

현재 최신 스프링 부트 2.1.0버전도 여전히 JUnit4를 기본으로 사용하고 있으므로
기본적인 테스트의 모양은 다음과 같다.

스프링부트테스트의 기본 골격
1
2
3
4
@RunWith(SpringRunner.class)
@SpringBootTest
public class SampleControllerTest {
}

1] Web Environment test

1>기초

아주 예전엔 서블릿 테스트를 위해 컨테이너 서버를 가동한 후 에러가 있으면 서버를 내리고 코드를 수정하고 다시 리부팅하는 사이클로 개발하였다. 이게 프로그램 규모가 커지거나. WAS같은 대단위 서버인 경우 키고 끄는것만 수분을 잡아먹게 된다. 심한 경우에는 개발 시간중에 코드짜는 시간보다 이 재가동 시간이 더 길게 되는 경우도 생긴다.

이를 해결하기 위한 방법이 목업이었다. 스프링3.2 이전에서 제공하는 서블릿 목업 구현체인 MockHttpServletRequest, MockHttpServletResponse를 사용하여 테스트를 함으로써
컨테이너 시간을 없애게 되었다.

1
2
3
4
5
6
7
8
9
TestController controller = new TestController();  
MockHttpServletRequest req = new MockHttpRequest();
MockHttpSerlvetResponse res = new MockHttpResponse();
ModelAndView mav = controller.form(req, res);
assertThat(res.getStatus(), is(200));
assertThat(res.getContentType(),
is(“text/plain”));
assertThat(res.getContentAsString (),
is(“content”));

하지만 테스트를 못하는 부분이 많았다.
일단 HttpServletRequest,HttpServletResponse 모두 @MVC 기반 어플리케이션에서
잘쓰지않고 @RequestMappingm @ModelAttribute 등도 처리가 불가능하다.
컨트롤러의 어노테이션 정보에는 매핑 방법, 데이터 추출 방법, 변환, 유효성 검증, 예외처리등 많은 것들이 있는데 이런 위의 단위테스트에선 전혀 반영이 되지 않는다.

이를 해결하고자 Spring MVC Test Framework가 따로 시작되었으며 spring-test-mvc로 관리되다가 스프링 3.2부터 대체된 모듈이 spring-test며 여기에 MockMvc가 등장하였다.

기존의 테스트 방법이 일일히 클래스와 모듈에 접근하였다면 Spring Test MVC에서는
테스트 클라이언트는 모든 인터렉션을 MockMVC객체와 한다.

실제 스프링 MVC가 Request후 response하는 부분을 Test에서 완전히 재현이 가능해서

이 mockmvc를 만들기 위해서는 스프링 빈설정에서 mock빈을 만들수 설정후
테스트에서 주입받거나 테스트의 @Before 메소드에서 만든다.

1
2
3
4
5
6
7
8
9
public class StandAloneTest {

private MockMvc mockMvc;
@Before
public void setup(){
this.mockMvc = MockMvcBuilders.standaloneSetup(new HomeController())
.build();
}
}

기초끝

2> Mock 테스트

‘@SpringBootTest’의 webEnvironment 어트리뷰트를 이용해서 쉽게 웹 테스트 환경을 선택할 수 있다. 기본값은 Mock이다. 따라서 ‘@SpringBootTest’어노테이션을 쓴다는 것은
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
효과가 동일하다.

위와 동일한 의미의 스프링 부트 테스트
1
2
3
4
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class SampleControllerTest {
}
  • Mock(Default)
    WebApplicationContext를 로드하며 서블릿 컨테이너가 아닌 Mock 서블릿을 제공한다.
    스프링부트는 1.4부터 MockMvc의 자동완성(auto-configuration)을 켜고 설정할 수 있는 @AutoConfigureMockMvc어노테이션을 제공하는데 이것을 함께 사용하면 정말 별다른 설정없이 간단하게 MockMVC를 사용할 수 있다.
세상에서 가장쉽게 MockMvc사용하는 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package me.rkaehdaos.springbootstudytest.sample;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class SampleControllerTest {
@Autowired
MockMvc mockMvc;

@Test
public void helloTest() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("hello GeunChang"))
.andDo(print());
}
}
3>실제 서버 테스트와 TestRestTemplate

webEnvironment 값을 RANDOM_PORT,DEFINED_PORT 로 주면 이제 목 객체가 아닌 실제 내장 임베디드 서버가 작동하게 된다. 목환경에서 mockMvc를 사용하였다면. 실제서버에서는
TestRestTemplate나 webTestClient를 사용하여야 한다.

RestTemplate
스프링 3.0부터 지원하며 스프링에서 제공하는 http통신에 유용한 템플릿으로 jdbctemplate처럼 가계적,반복적인 코드들을 깔끔하게 정리해준다. HTTP통신을 단순화하고 Restful 원칙을 지키며 json,xml 응답을 쉽게 받을 수 있다.

HTTP와의 다양한 통신

  • 자바1.2부터 java.net 패키지에 포함되어있는 URLConnection이 있다. GET,POST로 데이터 전달및 http이외의 프로토콜도 가능하나 응답코드가 4xx,5xx시 IOException이 떨어지고 타임아웃 설정 및 쿠키 제어가 불가능하다.
  • 아피치 프로젝트인 httpClient는 4.x때부터 Apache HttpComponent로 바뀌었다. 모든 응답코드를 읽을 수 잇고 타임아웃/쿠키제어가 가능하나 여전히 코드가 반복적이며 응답의 컨텐츠에 따라 별도 로직이 필요하다.
  • RestTemplate 이 HttpClient를 추상화해서 제공한다.

‘@SpringBootTest’와 TestRestTemplate을 사용하면 쉽게 리얼서버 테스트도 가능하다.
TestRestTemplate은 이 RestTemplate의 테스트를 위한 버전이다.

TestRestTemplate를 사용하는 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package me.rkaehdaos.springbootstudytest.sample;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleControllerTest {
@Autowired
TestRestTemplate testRestTemplate;

@Test
public void helloRealTest() throws Exception {
String result = testRestTemplate.getForObject("/hello",String.class);
assertThat(result).isEqualTo("hello GeunChang");
}
}
4> MockMvc 테스트와 TestRestTemplate 테스트 비교
  • 서블릿 컨테이너의 사용여부가 가장 큰차이
  • 입장의 차이
    • MockMvc는 서버 입장에서 구현한 API를 통해 비지니스 로직의 문제를 테스트
    • TestRestTemplate은 클라이언트 입장에서 RestTemplate 사용하듯이 테스트
  • 트랜잭션의 차이
    • spring-boot-test는 spring-test의 확장이라 @Transaction시 끝나고 롤백
    • TestRestTemplate 테스트의 실제 서버는 별도의 쓰레드 수행 -> 롤백안됨

3. 테스트 범위 제한

위의 테스트의 경우 컨트롤러에서 서비스까지 거쳐서 나오게 되는데
호출하는 서비스가 매우 무거운 서비스고 나는 컨트롤러만 테스트 하고 싶다면
어떻게 하면 될까? 서비스를 목 객체로 교체하면 될것이다. 이를 간단하게 할 수 있는 방법이 @MockBean이다. ‘@MockBean’은 ApplicationContext의 빈을 Mock객체로 교체하며 각 ‘@Test’마다 리셋된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleControllerTest {
@Autowired
TestRestTemplate testRestTemplate;

@MockBean
SampleService mockSampleService; //서비스가 mock으로 교체

@Test
public void helloMockTest() throws Exception {

when(mockSampleService.getName()).thenReturn("MockSample");

String result = testRestTemplate.getForObject("/hello", String.class);
assertThat(result).isEqualTo("hello MockSample");
}
}

4. WebTestClient

원본 > 자바지기님 포스트

Spring5에 새롭게 추가된 기능 중 유용한 기능 중의 하나가 WebClient로
Spring WebFlux라고 불리우는 스프링5의 리액티브 웹 프레임워크의 일부분이다.
이 WebClient를 활용하면 API에 대한 요청과 응답을 non blocking으로 비동기 처리할 수 있다. 기존의 RestTemplate은 Synchronous다. RestTemplate을 대체할 것으로 생각된다.

자바지기님도 그렇고 백기선님도 그렇고 많이 이방법을 추천한다.

api가 굉장히 손쉽게 되어있어서 굳이 비동기를 사용하지 않아도 쓰기가 좋으며
나중에 실제 REST 호출이 많아져서 비동기가 필요시에도 바꿀 필요가 없으니까

TestWebClient는 테스트용이다.

5. 슬라이스 테스트

지금까지 한 것은 어플리케이션 전체가 돌아가는 어마어마한 통합테스트다.
‘@SpringBootTest’가 ‘@SpringBootApplication’을 찾아서 처음에 배웠던 자동설정을 포함한 어마어마한 양의 빈 스캔및 등록이 일어나게 된다.

테스트 하고 싶은 빈들만 등록하고 싶을떄 슬라이스 테스트용 어노테이션을 사용한다.

  • @JsonTest
    테스트 객체의 Json의 입출력만 테스트 하고 싶을 때 사용.
    ObjectMapper와 ‘@JsonComponent’ 빈을 포함한
    Jackson의 테스트를 위한 모듈들을 자동으로 설정합니다.
  • @WebMvcTest
    딱 컨트롤러 하나만 테스트 하려 할때 사용.
    열어보면 @AutoconfigureWebMvc,@AutoConfigureMockMvc등이 존재한다.
    테스트에 사용할 @Controller 클래스와 필요한 클래스등을 스캔하며 MockMvc를 자동으로 설정하여 빈으로 등록한다. 거의 웹에 관련 빈만 등록하며 @Service, @Repository
    이쪽은 아예 등록이 되지 않는다. 그래서 @MockBean으로 의존성깨지는 것의 객체를
    만들어줘야 한다.
  • @WebFluxTest
  • @DataJpaTest

http://hyper-cube.io/2017/08/10/spring-boot-test-2/ 보면서 정리할것

6. OutputCapture

Junit의 Rule을 확장한 것으로
콘솔로 출력이 되는 모든 것을 캡쳐한다.

1
2
3
4
5
6
7
8
9
10
@Rule //junit 애노테이션
public OutputCapture outputCapture = new OutputCapture();
//반드시 public 이어야한다.

@Test
public void test() {

....
assertThat(outputCapture.toString()).contains("xxxx");
}

저 캡처에는 로그부터 시작해서 System.out까지 전부 다 들어가 있다.

Related POST

공유하기