본문 바로가기

더 나은 엔지니어가 되기 위해/지금은 안쓰는 자바

[스프링 부트 개념과 활용] 테스트

인프런에서 백기선님의 스프링부트 개념과 활용 강의를 듣고, 개인적으로 공부하며 핵심만 정리한 글입니다.

세팅

테스팅에 들어가기 앞서, Controller 와 Service 클래스를 미리 정의해놓자.

// SampleController.java

@RestController
public class SampleController {

  @Autowired
  private SampleService sampleService;

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

@Service
public class SampleService {
    public String getName() {
        return "heumsi";
    }
}

테스트 코드 작성

1) 가장 기본적인 형태

테스팅 코드를 만들기 위한 가장 기본 형태는 다음과 같다.

// SampleControllerTest.java

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

  @Test
  public hello_test() {

  }
}

2) mockMvc 사용

Controller 에 대한 테스트를 위해 요청을 주고받는 작업을 하기 위해서는

  • 테스트 환경을 MOCK 으로 만들어주고
  • mockMvc 를 사용할 수 있다.

예를 들면 다음과 같다.

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

  @Autowired
  MockMvc mockMvc;

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

3) testRestTemplate 사용

MOCK 테스팅 환경이 아니라, 랜덤 포트를 하나 열어두고 전체 테스팅 환경을 구동하려면,

  • 테스트 환경을 RANDOM_PORT 로 만들어주고
  • TestRestTemplate 을 사용한다.

예를 들면 다음과 같다.

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

  @Autowired
  TestRestTemplate testRestTemplate;

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

}

4) @MockBean 으로 일부 Bean만 Mock 사용하기

위 3번의 문제점은 모든 Bean 을 다 만들어 테스팅 환경이 무거워진다는 것이다.
위 코드는 클래스 이름에서 볼 수 있듯, Controller 에 대한 테스팅만 해보고자 하는 것이다.
그런데 Controller 내부에 Service 를 이용하는 코드가 있기 때문에, Service 로직에 대한 오류도 Controller 테스팅에서 잡힐 수 있다.

이를 분리하여 Controller 테스팅에만 집중하기 위해, 다음과 같이 Service 를 따로 Mock 하여 기존의 Service 를 오버라이딩 할 수 있다.
즉, ApplicationContext 에 이미 우리가 정의한 Bean 을 Mock 으로 만든 객체로 교체한다.

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

  @Autowired
  TestRestTemplate testRestTemplate;

  @MockBean
  SampleService mockSampleService; // Service 를 Mock 하여 오버라이딩

  @Test
  public void hello() throws Exception{
    when(mockSampleService.getName()).thenReturn("heumsi_mock");
    // Mock 한 Service 를 다음과 같이 수정할 수 있다.

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

}

5) webTestClient 으로 async 하게 테스트하기

webTestClient 으로 클라이언트 단에서의 요청하는 것을 시뮬레이션 해볼 수 있다.

먼저, spring-boot-starter-webfluxpom.xmldependency 에 추가하자.

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

그리고 다음과 같이 사용 가능하다.

@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("heumsi_mock");

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

슬라이싱 테스트

@SpringBootTest 는 테스팅할 때, 실제로 Spring 이 돌아가는 모든 환경과 Bean 을 구축한다. 따라서 일부 기능만 테스팅하고 싶을 때, 굳이 필요없는 리소스까지 모두 띄우는 것은 좀 오버스러울 수 있다.

일부 레이어만 잘라서 테스팅해볼 수 있는데, 이를 슬라이싱 테스트라고 한다.
다음과 같은 슬라이싱 테스트들을 제공한다.

  • @JsonTest
  • @WebMvcTest
  • @WebFluxTest
  • @DataJpaTest

예를 들어, @WebMvcTest 으로 웹과 관련된 빈들만 테스팅에 로딩하여 슬라이싱 테스트를 할 수 있다.

1) 단일 Controller 만 테스트

하나의 컨트롤러만 테스팅한다.
로딩되는 Bean 에 Service 가 포함되지 않기 때문에 @MockBean 으로 주입받아야 한다.

@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class) // SampleController 만 테스트
public class SampleControllerTest {

  @MockBean
  SampleService mockSampleService; 
  // 기본적으로 Service Bean 은 로드 되지 않기 때문에, 반드시 주입시켜줘야 함.

  @Autowired
  MockMvc mockMvc;
  // WebMvcTest 는 반드시 MockMvc 로 테스팅 해야함.

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

    mockMvc.perform(get("/hello"))
      .andExpect(content().string("hello heumsi_mock"));
  }
}

테스트 유틸

1) outputCapture 로 로그가 찍혀있는지 확인

예를 들어, SampleController.java 에 다음과 같이 콘솔 출력을 하는 코드를 넣었다고 해보자.

// SampleController.java

@RestController
public class SampleController {

  Logger logger = LoggerFactory.getLogger(SampleController.java)

  @Autowired
  private SampleService sampleService;

  @GetMapping("/hello")
  public String hello() {
    logger.info("heumsi")
    System.out.println("skip")

    return "hello " + sampleService.getName();
  }
}

이제 테스팅 코드에서 outputCapture 로 이 heumsiskip 이 출력되었는지 체크할 수 있다.

@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class) // SampleController 만 테스트
public class SampleControllerTest {

  @MockBean
  SampleService mockSampleService; 

  @Autowired
  MockMvc mockMvc;

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

    mockMvc.perform(get("/hello"))
      .andExpect(content().string("hello heumsi_mock"));

    // 해당 부분
    assertThat(outputCapture.toString())
      .contains("heumsi")
      .contains("skip");
  }
}