스프링부트 테스트

스프링부트 애플리케이션을 테스트하기 위해서는 spring-boot-starter-test를 test 스코프로 추가하는 것 부터 시작한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

기본적인 테스트 클래스의 형태는 다음과 같다.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class SampleControllerTest {
}
  • MOCK: mock servlet environment. 내장 톰캣을 구동하지 않음
  • RANDON_PORT, DEFINED_PORT: 내장 톰캣 사용
  • NONE: 서블릿 환경을 제공하지 않음

MOCK

@SpringBootTest의 속성인 WebEnvironment디폴트로 SpringBootTest.WebEnvironment.MOCK로 되어있어, 서블릿 컨테이너가 아닌 mock 서블릿을 실행한다. mock 서블릿을 사용하여 디스패처 서블릿에 요청을 보내는 것과 비슷하게 테스트를 할 수 있다. mock-up된 서블릿에 요청을 보내기 위해서는 MockMvc라는 클라이언트를 사용해야 한다.

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SampleControllerTest {
    
    @Autowired
    MockMvc mockMvc;
}

MockMvc

MockMvc를 사용하기 위한 방법은 여러가지가 있는데 @AutoConfigureMockMvc 을 추가한 뒤 MockMvc을 자동주입받는 방법이 가장 쉽다고 한다.

@RestController
public class SampleController {

    @Autowired
    private SampleService sampleService;

    @GetMapping("/hello")
    public String hello() {
        return "hello " + sampleService.getName();
    }
}

@Service
public class SampleService {

    public String getName() {
        return "jch";
    }
}

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class SampleControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello() throws Exception {
        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello jch"))
                .andDo(print());
    }
}

MockMvc를 이용하면 서블릿(컨트롤러) 요청에 대한 응답을 테스트할 수 있다.

print()를 통해 출력되는 대부분의 내용을 테스트할 수 있다.

@SpringBootTest의 WebEnvironment에 RANDON_PORT나 DEFINED_PORT를 지정하면 mock-up된 서블릿이 아닌 서블릿 컨테이너인 내장 톰캣이 구동된다. 이런 경우에는 클라이언트로 RestTemplate이나 WebClient를 사용해야 한다.

RestTemplate

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class SampleControllerTest {

    @Autowired
    TestRestTemplate testRestTemplate;

    @Test
    public void hello() throws Exception {
        String result = testRestTemplate.getForObject("/hello", String.class);
        assertThat(result).isEqualTo("hello jch");
    }
}

TestRestTemplate를 사용하는 경우의 문제는 테스트 환경이 너무 커진다는 것이다. 컨트롤러에 대한 테스트만 작성하고 싶은데, 서비스가 관여하게된다. 즉, 서비스의 완전한 구현체가 반드시 존재해야 한다. 서비스에 상관없이 컨트롤러만 테스트하고 싶은 경우 MockBean을 사용할 수 있다.

MockBean

@MockBean
SampleService mockSampleService;

@MockBean을 사용하면 ApplicationContext에 들어있는 SampleService의 빈을 MockBean으로 교체한다. 컨트롤러는 등록된 SampleService의 빈이 아닌 교체된 mockSampleService를 사용하게 된다.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class SampleControllerTest {

    @Autowired
    TestRestTemplate testRestTemplate;

    @MockBean
    SampleService mockSampleService;

    @Test
    public void hello() throws Exception {
        when(mockSampleService.getName())
                .thenReturn("jch"); // 1

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

MockBean을 사용할 때는, // 1과 같이 메소드의 동작을 입력해야 한다. 위 코드는 mockSampleService.getName()가 호출되면 "jch"가 반환된다. MockBean을 사용하면 구현체가 없어도 테스트가 가능해진다.

WebClient

WebTestClient는 SpringMvc WebFlux에 추가된 rest 클라이언트 중 하나이다. 기존의 rest 클라이언트는 동기(synchronous) 방식으로 동작하지만 WebTestClient는 비동기(asynchronous) 방식으로 동작한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

WebTestClient를 사용하기 위해서는 spring-boot-starter-webflux 의존성을 추가해야 한다.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class SampleControllerTest {

    @Autowired
    WebTestClient webTestClient;

    @MockBean
    SampleService mockSampleService;

    @Test
    public void hello() throws Exception {
        when(mockSampleService.getName())
                .thenReturn("jch");

        webTestClient.get().uri("/hello").exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("hello jch");
    }
}

강사님은 비동기 테스트를 사용하지 않더라도, TestRestTemplate의 API는 사용하기가 불편하고, WebTestClient의 API는 사용하기가 좋아서 WebTestClient를 사용할 것 같다고 한다. TestRestTemplate을 이용할 때, status().isOk()와 같은 테스트 코드를 작성할 때 자동완성이 지원되지 않아서 불편하다. 비동기 로직이 많다면 성능상으로도 이점이 많기 떄문에 WebTestClient에 익숙해지는게 좋다고 한다.

두번째로 편한건 MockMvc라고 언급한다

@SpringBootTest

@SpringBootTest가 @SpringBootApplication를 찾아가서 테스트용 ApplicationContext를 만들면서 빈을 자동으로 등록한다. 이후 MockBean을 찾아서 교체한다. MockBean은 테스트마다 리셋되므로 우리가 직접 리셋을 관리할 필요가 없다.

슬라이스 테스트

@SpringBootTest가 장황하게 수많은 빈을 등록하는 것이 싫다면 레이어별로 잘라서 테스트할 수 있다. 다음과 같은 슬라이스 테스트용 어노테이션을 용도에 맞게 사용하면 된다.

  • @JsonTest
    • Json 응답만 테스트할 수 있다.
  • @WebMvcTest
    • 빈 하나만 테스트할 수 있다.
    • 웹과 관련된 빈(컨트롤러, 필터 등)만 등록된다.
    • 사용하는 의존성이 있다면 MockBean을 만들어서 채워줘야 된다.
  • @WebFluxTest
  • @DataJpaTest

테스트 유틸

스프링부트는 아래와 같은 테스트 유틸을 제공한다.

  • OutputCapture
  • TestPropertyValues
  • TestRestTemplate
  • ConfigFileApplicationContextInitializer

OutputCapture만 살펴보자. OutputCapture는 로그를 비롯해서 콘솔에 찍히는 모든 것을 캡쳐한다.

@RestController
public class SampleController {

    Logger logger = LoggerFactory.getLogger(SampleController.class);

    @Autowired
    private SampleService sampleService;

    @GetMapping("/hello")
    public String hello() {
        logger.info("logger jch");
        System.out.println("sout jch");
        return "hello " + sampleService.getName();
    }
}

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class SampleControllerTest {

    @Rule
    public OutputCapture outputCapture = new OutputCapture();

    @Autowired
    WebTestClient webTestClient;

    @MockBean

    SampleService mockSampleService;

    @Test
    public void hello() throws Exception {
        when(mockSampleService.getName())
                .thenReturn("jch");

        webTestClient.get().uri("/hello").exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("hello jch");

        assertThat(outputCapture.toString())
                .contains("logger")
                .contains("sout");
    }
}

OutputCapture은 JUnit의 Rule를 확장해서 만들어졌다. OutputCapture는 @Rule의 제약사항때문에 public 접근제어자를 지정해야한다. OutputCapture를 이용하면 로그 메시지를 이용한 테스트를 작성하기가 쉬워진다. 로그 메시지를 중간 중간 중요한 부분에 출력하는 코드를 작성하고, 해당 로그가 출력되었는지 테스트할 수 있다.

해당 포스팅은 스프링 부트 개념과 활용 강의 내용을 토대로 작성하였습니다.