글 작성자: 자바니또

스프링 부트에서 테스트 코드를 작성하자

개요

견고한 서비스를 만들고 싶은 개발자나 팀에선 TDD를 하거나 최소한 테스트 코드는 꼭 작성해야한다. 프로젝트를 유지보수하거나 리팩터링하는데 있어서 테스트 코드는 절대 빠질 수 없는 요소이다. 이번 장에서는 테스트 코드 작성의 기본을 배운다.

참고로 Spring Boot의 버전은 2.1.7 이다.

목차

  • 테스트 코드 소개(단위 테스트와 TDD)
  • 테스트 코드의 장점
  • @SpringBootTest
  • @RunWith
  • 테스트 코드 작성
    • Sample Code
    • @MockMvc로 Controller테스트
    • TestRestTemplate사용하여 내장 톰캣 환경에서 테스트
    • @MockBean으로 일부 빈을 MockBean으로 대체하기
    • @WebMvcTest로 슬라이싱 테스트하기

테스트 코드 소개(단위 테스트와 TDD)

우선 TDD단위테스트가 서로 다르다는 것을 알고 넘어가자. TDD와 단위 테스트는 근본적으로 다른데, TDD는 개발에 대한 하나의 방법론 이고 단위 테스트는 말 그대로 테스트기법의 종류 중 하나 이다.

TDD는 테스가 주도하는 개발을 이야기 하며, 테스트 코드를 먼저 작성하는 것 부터 시작한다. TDD의 원칙은 다음과 같다.

  1. 테스트 코드를 먼저 작성한다. (Red) 당연히 테스트 코드는 실패한다. 이때 주의할 것은 너무 복잡한 부분보단 간단한 부분 부터 작성하는게 좋다.
  2. 테스트 코드를 통과하는 프로덕션 코드를 작성한다. (Green) 프로덕션 코드를 작성할 때는 깔끔함 보다는 실행이 되는 것에 중점을 두어 작성한다.
  3. 테스트가 동과하면 프로덕션 코드를 리팩토링한다.(Refactoring)

반면 단위 테스트는 기능 단위의 테스트 코드를 작성하는 것을 이야기하며, 테스트 코드를 꼭 먼저 작성해야 하는 것도 아니고, 리팩토링도 포함되지 않는다.

이번 장에서는 TDD가 아닌 단위 테스트 코드를 배운다. TDD를 좀더 알고 싶다면 다음 링크를 참고하기 바란다. 'TDD 실천법과 도구' 공개 PDF (https://repo.yona.io/doortts/blog/issue/1)

단위 테스트 코드의 장점

위키피디아에서는 단위 테스트 코드를 작성함으로써 얻는 이점으로 다음을 이야기 한다.

  • 개발단계 초기에 문제를 발견하게 도와준다.
  • 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수있다.(회귀 테스트)
  • 기능에 대한 불확실성을 감소시킨다.
  • 시스템에 대한 실제 문서를 제공한다. 즉, 단위 테스트 자체가 해당 애플리케이션의 기능 문서로 사용될 수 있다.

이 책의 저자가 말하는 장점은 다음과 같다.

  • 빠른 피드백을 받을 수 있다. 테스트 코드를 작성해 놓으면 매번 코드를 수정할 때마다 프로그램을 재실행 하여 눈으로 확인 할 필요가 없다.
  • 눈으로 검증할 필요가 없다. System.out.println()으로 콘솔창을 눈으로 검증할 필요 없이 테스트코드를 작성한다면 자동 검증을 해준다.
  • 개발자가 만든 기능을 안전하게 보호해 준다. 새로운 기능이 추가 될 때 이전까지 잘 되던 기능들을 빠르게 테스트 할 수있어서 개발자는 마음 놓고 코드를 수정할 수 있다.

Test Annotation

스프링부트에서 테스트코드를 작성할 때 자주 사용되는 애노테이션에 대해서 알아보자.

@SpringBootTest

스프링 부트에서는 @SpringBootTest를 통해 어플리케이션 테스트에 필요한 거의 모든 의존성을 제공해준다. @SpringBootTest가 하는 일은 다음과 같다.

  1. @SpringBootApplication을 찾아가 하위의 모든 Bean을 Scan한다.
  2. 테스트용 Application Context를 만들어 빈을 등록한다.
  3. 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"));  
    }  
}

참고