부트 개념과 활용-13.테스트/테스트 유틸


테스트

시작은 일단 spring-boot-starter-test 의존성 추가

@SpringBootTest

  • @RunWith(SpringRunner.class)랑 같이 써야 함. (@ExtendWith(SpringExtension.class) // junit 5 기준)
  • 빈 설정 파일은 설정을 안해주나? 알아서 찾습니다. (@SpringBootApplication)
  • webEnvironment
    • MOCK: mock servlet environment. 내장 톰캣 구동 안 함.
    • RANDON_PORT, DEFINED_PORT: 내장 톰캣 사용 함.
    • NONE: 서블릿 환경 제공 안 함.

@MockBean

  • ApplicationContext에 들어있는 빈을 Mock으로 만든 객체로 교체 함.
  • 모든 @Test 마다 자동으로 리셋.

예제 코드

@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 "jaeuk";
    }
}
@ExtendWith(SpringExtension.class) // junit 5 기준
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
// 서블릿 컨테이너를 테스트용으로 띄우지 않고, 서블릿을 모킹한게 뜬다.
// 디스패처 서블릿이 뜨긴 하는데, 디스패처 서블릿으로 요청을 보내는것과 비슷하게 실행은 할 수 있는데, 목업이 된 서블릿이 인터렉션 하려면 MockMvc 클라이언트를 사용해야 한다.
@AutoConfigureMockMvc // MockMvc를 만드는 가장 쉬운 방법
class SampleControllerTest {
    @Autowired
    MockMvc mockMvc;

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

랜덤포트 사용할때 서블릿 컨테이너가(내장 톰켓이) 뜬다.
이땐 MockMvc가 이 아니라 TestRestTemplate이나 WebTestClient를 써야 한다.

TestRestTemplate 사용

@ExtendWith(SpringExtension.class) // junit 5 기준
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 랜덤포트 사용할때 서블릿 컨테이너가(내장 톰켓이) 뜬다.
// 이땐 MockMvc가 이 아니라 TestRestTemplate이나 WebTestClient를 써야 한다.     
class SampleControllerTest {
    @Autowired
    TestRestTemplate testRestTemplate; // 이상태에선 주입 가능

    // 근데 이 컨트롤러가 서비스까지 연결되어 있다.(테스트 커짐)
    @MockBean
    SampleService mocSampleService; // 목빈으로 설정하면 sampleService 를 mockSampleService 만들어서 쓰는 것

    @Test
    public void hello() throws Exception{
        when(mocSampleService.getName()).thenReturn("jaeuk"); // getNaem 이 호출되면 jaeuk 리턴받는다
        // SampleService 얘를 모킹해서 @MockBean 을 사용해 그 빈을 교체한 것.

        String result = testRestTemplate.getForObject("/hello", String.class);
        assertEquals(result, "hello jaeuk");
    }
}

WebTestClient 사용

@ExtendWith(SpringExtension.class) // junit 5 기준
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 랜덤포트 사용할때 서블릿 컨테이너가(내장 톰켓이) 뜬다.
// 이땐 MockMvc가 이 아니라 TestRestTemplate이나 WebTestClient를 써야 한다. 
class SampleControllerTest {

    // (스프링 5에서 새로 추가) 스프링 웹 플럭스 쪽에 추가된 레스트 클라이언트 중에 하나.
    // 기존 사용하던 레스트 클라이언트는 Synchronous(동기) - 요청 하나 보내고 기존에 하나 끝나야 다시 하나 보낼 수 있다.
    // asynchronous(비동기) 하다. 요청을 보내고 기다리는게 아니라, 응답이 오면 그때 바로 콜백이 온다.
    // 그때 이벤트가 오기 때문에 콜백을 바로 실행할 수 있다.
    // 테스트 코드에서도 웹 클라이언트랑 동일한 api를 사용 가능하다.
    @Autowired
    WebTestClient webTestClient; // webflux 의존성 추가해줘야 함.

    // 근데 이 컨트롤러가 서비스까지 연결되어 있다.(테스트 커짐)
    @MockBean
    SampleService mocSampleService; // 목빈으로 설정하면 sampleService 를 mockSampleService 만들어서 쓰는 것

    @Test
    public void hello() throws Exception{
        when(mocSampleService.getName()).thenReturn("jaeuk"); // getNaem 이 호출되면 jaeuk 리턴받는다
        // SampleService 얘를 모킹해서 @MockBean 을 사용해 그 빈을 교체한 것.

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

통합 테스트
@SpringBootTest 얘가 @SpringBootApplication를 찾아서
거기서부터 시작하는 모든 빈을 스캔하고,
모든 걸 테스트용 애플리케이션 컨텍스트를 만들면서 모든 빈을 다 등록 후,
목으로 된 빈만 찾아서 목 빈으로 교체해준다.
목 빈은 테스트마다 리셋되니까 처음부터 모킹을 하면서 시작해도 지장이 없다.

수많은 빈들이 등록되는 건 싫고,
그럼 난 내가 테스트하고 싶은 것만 등록하고 싶다면.

슬라이스 테스트용 애노테이션

슬라이스 테스트

  • 레이어 별로 잘라서 테스트하고 싶을 때
  • @JsonTest (json 타입 테스트)
  • @WebMvcTest (내가 하고싶은 컨트롤러 하나만 등록 가능)

    하나만 사용하니까 더 빠름

@ExtendWith(SpringExtension.class) // junit 5 기준
@WebMvcTest(SampleController.class) // 웹과 관련된 부분만 빈으로 등록이 된다.
// 일반적인 컴포넌트는 빈으로 등록 안됨(서비스, 레파지토리 등 x) 사용하는 의존성이 있다면 목 빈으로 채워줘야 함
class SampleControllerTest {

    @MockBean
    SampleService mocSampleService; // 목빈으로 설정하면 sampleService 를 mockSampleService 만들어서 쓰는 것

    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello() throws Exception{
        when(mocSampleService.getName()).thenReturn("jaeuk"); // getNaem 이 호출되면 jaeuk 리턴받는다

        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello jaeuk"))
                .andDo(print());
    }
}
  • @WebFluxTest
  • @WebMvcTest

    웹과 관련된 부분만 빈으로 등록이 된다.

  • @DataJpaTest

    레파지토리만 빈 등록이 됨

테스트 유틸

OutputCapture

@RestController
public class SampleController {

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

    @Autowired
    private SampleService sampleService;

    @GetMapping("hello")
    public String hello(){
        logger.info("ju.choi");
        System.out.println("skip"); // 이것도 테스트가 된다.(캡쳐된다)
        return "hello " + sampleService.getName();
    }
}
@ExtendWith({SpringExtension.class, OutputCaptureExtension.class}) // junit 5 기준 (OutputCaptureExtension )
@WebMvcTest(SampleController.class)
class SampleControllerTest {

    // @Rule // junit 5에서 rule기반 어노테이션 변경됨.
    // public OutputCaptureRule outputCaptureRule = new OutputCaptureRule();
    // 로그를 비롯, 콘솔에 찍히는 모든것을 다 캡쳐한다 

    @MockBean
    SampleService mocSampleService; // 목빈으로 설정하면 sampleService 를 mockSampleService 만들어서 쓰는 것

    @Autowired
    MockMvc mockMvc;

    @Test
    public void hello(CapturedOutput capturedOutput) throws Exception{
        when(mocSampleService.getName()).thenReturn("jaeuk"); // getNaem 이 호출되면 jaeuk 리턴받는다

        mockMvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string("hello jaeuk"))
                .andDo(print());

        System.out.println(capturedOutput.toString().contains("ju.choi"));
        System.out.println(capturedOutput.toString().contains("skip"));
    }
}





© 2019. by jaeuk