스프링 부트에서 테스트 코드 작성하기
스프링 부트에서 테스트 코드를 작성하자
개요
견고한 서비스를 만들고 싶은 개발자나 팀에선 TDD를 하거나 최소한 테스트 코드는 꼭 작성해야한다. 프로젝트를 유지보수하거나 리팩터링하는데 있어서 테스트 코드는 절대 빠질 수 없는 요소이다. 이번 장에서는 테스트 코드 작성의 기본을 배운다.
참고로 Spring Boot의 버전은 2.1.7 이다.
목차
- 테스트 코드 소개(단위 테스트와 TDD)
- 테스트 코드의 장점
- @SpringBootTest
- @RunWith
- 테스트 코드 작성
- Sample Code
- @MockMvc로 Controller테스트
- TestRestTemplate사용하여 내장 톰캣 환경에서 테스트
- @MockBean으로 일부 빈을 MockBean으로 대체하기
- @WebMvcTest로 슬라이싱 테스트하기
테스트 코드 소개(단위 테스트와 TDD)
우선 TDD
와 단위테스트
가 서로 다르다는 것을 알고 넘어가자. TDD와 단위 테스트는 근본적으로 다른데, TDD는 개발에 대한 하나의 방법론 이고 단위 테스트는 말 그대로 테스트기법의 종류 중 하나 이다.
TDD
는 테스가 주도하는 개발을 이야기 하며, 테스트 코드를 먼저 작성하는 것 부터 시작한다. TDD의 원칙은 다음과 같다.
- 테스트 코드를 먼저 작성한다. (Red) 당연히 테스트 코드는 실패한다. 이때 주의할 것은 너무 복잡한 부분보단 간단한 부분 부터 작성하는게 좋다.
- 테스트 코드를 통과하는 프로덕션 코드를 작성한다. (Green) 프로덕션 코드를 작성할 때는 깔끔함 보다는 실행이 되는 것에 중점을 두어 작성한다.
- 테스트가 동과하면 프로덕션 코드를 리팩토링한다.(Refactoring)
반면 단위 테스트
는 기능 단위의 테스트 코드를 작성하는 것을 이야기하며, 테스트 코드를 꼭 먼저 작성해야 하는 것도 아니고, 리팩토링도 포함되지 않는다.
이번 장에서는 TDD가 아닌 단위 테스트 코드를 배운다. TDD를 좀더 알고 싶다면 다음 링크를 참고하기 바란다. 'TDD 실천법과 도구' 공개 PDF (https://repo.yona.io/doortts/blog/issue/1)
단위 테스트 코드의 장점
위키피디아에서는 단위 테스트 코드를 작성함으로써 얻는 이점으로 다음을 이야기 한다.
- 개발단계 초기에 문제를 발견하게 도와준다.
- 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수있다.(회귀 테스트)
- 기능에 대한 불확실성을 감소시킨다.
- 시스템에 대한 실제 문서를 제공한다. 즉, 단위 테스트 자체가 해당 애플리케이션의 기능 문서로 사용될 수 있다.
이 책의 저자가 말하는 장점은 다음과 같다.
- 빠른 피드백을 받을 수 있다. 테스트 코드를 작성해 놓으면 매번 코드를 수정할 때마다 프로그램을 재실행 하여 눈으로 확인 할 필요가 없다.
- 눈으로 검증할 필요가 없다. System.out.println()으로 콘솔창을 눈으로 검증할 필요 없이 테스트코드를 작성한다면 자동 검증을 해준다.
- 개발자가 만든 기능을 안전하게 보호해 준다. 새로운 기능이 추가 될 때 이전까지 잘 되던 기능들을 빠르게 테스트 할 수있어서 개발자는 마음 놓고 코드를 수정할 수 있다.
Test Annotation
스프링부트에서 테스트코드를 작성할 때 자주 사용되는 애노테이션에 대해서 알아보자.
@SpringBootTest
스프링 부트에서는 @SpringBootTest
를 통해 어플리케이션 테스트에 필요한 거의 모든 의존성을 제공해준다. @SpringBootTest가 하는 일은 다음과 같다.
- @SpringBootApplication을 찾아가 하위의 모든 Bean을 Scan한다.
- 테스트용 Application Context를 만들어 빈을 등록한다.
- mock bean을 찾아가 그 빈만 mock bean으로 교체한다.
또, webEnvironment
로 다음의 속성 값들을 가질 수 있다.
- MOCK
: 내장 톰캣을 사용하지 않고 Mock Servlet환경에서 테스팅. - RANDOM_PORT / DEFINED_PORT
: (랜덤포트 / 정의된 포트)로 내장된 톰캣을 사용한 환경에서 테스팅. - NONE
: Servlet환경을 제공하지 않는다.
@RunWith
@RunWith
는 테스트를 진행할 때 JUnit프레임워크에 내장된 실행자말고 다른 실행자로 실행시킬 때 사용한다. 여기서는 SpringRunner라는 스프링 실행자를 사용하여 테스트를 진행한다. 즉, 스프링부트 테스트와 JUnit 사이에 연결자 역할을 한다.
테스트 코드 작성
샘플 코드를 하나 작성하고 테스트 목적에 따라 어떻게 테스트 코드를 짤 수 있는지 알아보자.
Sample code
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String hello() {
return "hello " + helloService.getName();
}
}
@Service
public class HelloService{
public String getName() {
return "javanitto";
}
}
@MockMvc로 Conroller 테스트
컨트롤러의 기능만 테스트하면 되기 때문에 내장 톰캣을 구동하지 않고 WebEnvironment.MOCK
을 통해 HttpRequest를 받는 서블릿을 MOCK Servlet으로 생성한다. 그리고 @AutoConfigureMockMvc
를 사용하여 MockMvc를 빈으로 등록해준다. status()와 content()는 MockMvcResultMatchers의 메서드들이고, get()은 MockMvcRequestBuilders의 메서드이다.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class HelloControllerTest {
@Autowired
MockMvc mockMvc;
@Test
public void hello_javanitto를_출력한다() throws Exception {
mockMvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("hello javanitto"))
.andDo(print());
}
}
TestRestTemplate사용하여 내장 톰캣 환경에서 테스트
TestRestTemplate
은 MOCK환경이 아니라 내장 톰캣을 구동한 환경에서 테스트한다. Application Context에 모든 빈이 등록되므로 테스트 환경이 무겁다.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void hello_javanitto를_출력한다() throws Exception {
String result = restTemplate.getForObject("/hello", String.class);
assertThat(result).isEqualTo("hello javanitto");
}
}
@MockBean으로 일부 빈을 MockBean으로 대체하기
@MockBean을 달아 놓으면 @SpringBootTest에 의해 테스트용 ApplicationContext에 등록되어 있는 빈을 Mock Bean으로 대체한다. Mockito.when()을 사용하여 mock bean의 행위를 정할 수 있다.
지금까지의 방법은 HelloService의 결과가 HelloController의 테스트에 영향을 미친다. 이때 HelloService를 MockBean으로 대체하여 테스팅한다면 오롯이 HelloController의 기능만 테스트 할 수 있다.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloControllerTest {
@Autowired
private TestRestTemplate restTemplate;
@MockBean
private HelloService mockHelloService;
@Test
public void hello_javanitto를_출력한다() throws Exception {
when(mockHelloService.getName()).thenReturn("javanitto2");
String result = restTemplate.getForObject("/hello", String.class);
assertThat(result).isEqualTo("hello javanitto2");
}
}
@WebMvcTest로 슬라이싱 테스트하기
@SpringBootTest는 실제로 애플리케이션이 돌아가는 모든 환경과 빈을 구축한다. 단위 테스트를 할 때 이것은 테스팅 환경을 무겁게 할 뿐이다. 스프링부트에서는 일부 레이어만 잘라서 테스트할 수 있는데, 이것을 슬라이싱 테스트라한다.
스프링부트는 애노테이션을 통해 간편하게 슬라이싱 테스트를 할 수 있게 지원한다. @WebMvcTest
, @JsonTest
, @WebFluxTest
, @DataJpaTest
가 그 애노테이션 들이다. 이 중 @WebMvcTest에 대해 알아 보자.
@WebMvcTest
는 지정한 컨트롤러들만 Application Context에 빈으로 등록한다. 지정하지 않으면 @Controller와 @ControllerAdvice 등을 달아놓은 클래스들을 빈으로 등록한다. 이때 @Service, @Component, @Repository등은 빈으로 등록하지 않기 때문에 Mock으로 생성해주어야 한다. 또한 @WebMvcTest를 사용할 때는 반드시 MockMvc를 사용하여야 한다. 이 때 @AutoConfigureMockMvc는 사용하지 않아도 된다.
@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
@Autowired
private MockMvc mvc;
@MockBean
private HelloService mockService;
@Test
public void hello_javanitto를_출력한다() throws Exception {
when(mockService.getName()).thenReturn("javanitto");
mvc.perform(get("/hello"))
.andExpect(status().isOk())
.andExpect(content().string("hello javanitto"));
}
}