SpringBootTest 정리

이지수·2022년 5월 9일
1

테스트 관련 어노테이션 정리

테스트 코드를 자다보면 @SpringBootTest, @Runwith, @Mock, @MockBean, @Spy, @InjectMock 등의 다양한 어노테이션을 볼 수 있다.
JUnit4, Junit5에 따라서 조금 사용방법이 다른점이 있고,
SpringBoot버젼에 따라서도 조금 다른점이 있다. 정리를 해보고자 함.


@SpringBootTest

주요역할

  • @SpringBootApplication을 찾아서 테스트를 위한 Bean을 생성한다.
  • @MockBean으로 정의된 Bean을 찾아서 대체시킨다.
  • @RunWith(SpringRunner.class)와 같이 정의하여야 동작한다. (Junit5에서 생략가능)

options

  • properties: application.yml에 지정된 properties를 재정의한다.
  • classes: @Configuration이 정의된 Class를 세팅하여 Bean생성 범위를 직접 정의한다.
    • 지정되지 않는 경우 AutoConfiguration으로 자동으로 모든 Bean이 생성된다.
  • webEnviroment: 테스트 환경을 선택한다.
    • MOCK(default)
      • WebApplicationContext를 로드하고 서블릿컨테이너 대신 Mock서블릿을 제공
      • @AutoConfigureMockMvc와 함께 지정하여 MockMvc를 사용한 테스트 수행시 사용
    • RANDOM_PORT
      • EmbeddedWebApplicationContext를 로드하여 실제 서블릿 환경 구성. 임의의 포트 listen
    • DEFINE_PORT
      • RANDOM_PORT와 동일하지만 실제 application.yml에서 지정한 포트사용
    • NONE
      - 일반적인 ApplicationContext를 로드하여 서블릿 환경을 구성하지 않음

@RunWith

주요역할

  • JUnit의 테스트 실행방법을 정의 하는 어노테이션
    • org.junit.runner를 상속받은 객체를 정의한다.
    • 실제 테스트 클래스를 호출하는 책임이 있는 클래스
  • @Autowired, @MockBean등을 로딩하는 주체
    • @SpringBootTest는 AppCtx를 전부 구성하고 Spring관련 Configuration을 수행하고,
    • @RunWith(SpringRunner.class)는 테스트 파일에 정의된 @Autowired, @MockBean등만 AppCtx로 로딩하는 역할을 수행.
    • Mock과 Autowired등을 사용할 수 있게 해주는 브릿지 역할

JUnit5에서는 @ExtendWith(SpringExtension.class) 으로 변경되었으며, @SpringBootTest 내부에 내장되어 있기 때문에 명시적으로 작성하지 않아도 좋다. (어차피 대부분의 Junit4에서의 테스트의 경우 둘다 정의하기 때문에 내장시킨 것 같다)
아래 나오는 Example 은 Junit5를 기준으로 작성되었다.


👎@WebMvcTest

주요역할

  • 컨트롤러의 동작만 확인하는 경우 사용된다.
  • 아래와 같이 컨트롤러와 관련된 어노테이션 및 클래스만 로딩한다.
  • 그렇기 때문에 컨트롤러에서 호출하는 Service객체등은 Mock으로 정의해야 한다.
  • 그닥 잘 사용되지는 않을 것 같다...
@Controller, @RestController, @ControllerAdvice, @JsonComponent,
Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, HandlerMethodArgumentResolver

업무하면서 한번도 써본적이 없는 것 같은데,
아마도 써야한다면 RequestParams, PathVariable의 검증이나 에러케이스의 응답처리 확인용으로 쓴다면 쓸 것 같긴 한데...
애초에 컨트롤러 보다는 서비스 단위로 단위테스트 돌리는 케이스가 많은듯!?

Example

// Junit5 버젼이기 때문에 @RunWith필요 없다. @WebMvcTest에도 내장
@WebMvcTest(ProductController.class)
public class ProductControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private ProductService productService;

    @DisplayName("상품조회 테스트")
    @Test
    public void getProduct() throws Exception {
        final List<String> list = List.of("갤럭시");

        when(productService.getProduct()).thenReturn(list);

        mvc.perform(get(("/product/all")))
                .andExpect(status().isOk())
                .andExpect(content().json("[\"갤럭시\"]"))
                .andDo(print());
    }

    @DisplayName("상품등록 테스트")
    @Test
    public void insertProduct() throws Exception {
        mvc.perform(post(("/product/insert"))
                .param("productName", "아이패드"))
                .andExpect(status().isOk())
                .andExpect(content().string("OK"))
                .andDo(print());
    }

}

👍@AutoConfigurationMockMvc

주요역할

  • @WebMvcTest와 사용방법은 유사하지만,
  • @Service, @Repository등의 Bean도 같이 로딩한다.
  • @SpringBootTest와 함께 사용된다.
    • webEnvironment 옵션을 통해서 실제 WAS를 띄울지 MOCK서버를 띄울지 결정한다.

정리하면 간단히 컨트롤러만 테스트 하는 경우 @WebMvcTest
Service와 Repository도 같이 돌려보는 경우 @AutoCongifurationMockMvc + @SpringBootTest

Example

@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    private ProductService productService;

    @Order(2)
    @DisplayName("[GET] /product/all")
    @Test
    public void getProduct() throws Exception {
        // then
        mvc.perform(get(("/product/all")))
                .andExpect(status().isOk())
                .andExpect(content().json("[\"아이패드\"]"))
                .andDo(print());
    }

    @Order(1)
    @DisplayName("[POST] /product/insert")
    @Test
    public void insertProduct() throws Exception {
        mvc.perform(post(("/product/insert"))
                .param("productName", "아이패드"))
                .andExpect(status().isOk())
                .andExpect(content().string("OK"))
                .andDo(print());
    }

    @DisplayName("[POST] /product/insert (파라메터 없는 경우 에러케이스)")
    @Test
    public void insertProduct2() throws Exception {
        mvc.perform(post(("/product/insert")))
                .andExpect(status().isBadRequest()) // 400 에러 검증
                .andDo(print());
    }

}

@Mock vs @MockBean

@MockBean과 @Mock모두 stub 가능한 Mock 객체를 생성하는 역할을 한다.
MockBean은 스프링 하위 패키지에 존재하여 스프링 전용 Mock이라고 보면 되는데,
간단히 말하면 MockBean으로 정의한 객체는 AppCtx구성시 BeanFactory에 실제 Bean대신 MockBean을 생성하여 로딩한다.
Mock의 경우는 AppCtx, BeanFactory에 포함되지 않고 테스트 코드상에서 주입하거나 @InjectMock등의 추가 어노테이션으로 의존성 관리를 직접 해야한다.

@Mock

  • AppCtx구성시 BeanFactory에 포함되지 않는다.
  • @InjectMock이나, Setter, 생성자등으로 의존성을 갖는 객체에 주입시켜주어야 한다.
@SpringBootTest
class ProductServiceMockTest {
    
    @InjectMocks
    private ProductService productService;

    @Mock
    private ProductRepository productRepository;

    @Test
    void getProduct() {
        // given (stub 처리, 테스트를 위한 데이터 준비)
        final List<String> mockResult = Lists.newArrayList();
        mockResult.add("갤럭시");
        when(productRepository.getProduct()).thenReturn(mockResult);

        //when (테스트 수행)
        final List<String> results = productService.getProduct();

        // then (검증)
        assertThat(results).isNotNull();
        assertThat(results).isNotEmpty();
        assertThat(results).hasSize(1);
        assertEquals(mockResult, results);
    }

    @Test
    void insertProduct() {
        // given (stub 처리, 테스트를 위한 데이터 준비)
        final String product = "헬로우";

        //when (테스트 수행)
        productService.insertProduct(product);

        // then (검증)
        verify(productRepository, times(1)).insertProduct(product);
    }
}

@MockBean

  • Spring전용 Mock객체
  • ContextLoading시점에 실제 Bean대신에 MockBean이 존재하는 경우 대체 한다.
  • 별다른 @InjectMock 없이 Spring DI 시점에 자동으로 MockBean이 주입된다.

일반적으로 대부분의 단위 테스트가 이런 모습이 될 것 같다.
참고) ProductRepository가 Mock이 아니라면 그냥 지워버리면 Repository가 그대로 동작

@SpringBootTest
class ProductServiceMockTest {

    @Autowired
    private ProductService productService;

    @MockBean
    private ProductRepository productRepository;

    @Test
    void getProduct() {
        // given (stub 처리, 테스트를 위한 데이터 준비)
        final List<String> mockResult = Lists.newArrayList();
        mockResult.add("갤럭시");
        when(productRepository.getProduct()).thenReturn(mockResult);

        //when (테스트 수행)
        final List<String> results = productService.getProduct();

        // then (검증)
        assertThat(results).isNotNull();
        assertThat(results).isNotEmpty();
        assertThat(results).hasSize(1);
        assertEquals(mockResult, results);
    }

    @Test
    void insertProduct() {
        // given (stub 처리, 테스트를 위한 데이터 준비)
        final String product = "헬로우";

        //when (테스트 수행)
        productService.insertProduct(product);

        // then (검증)
        verify(productRepository, times(1)).insertProduct(product);
    }
}

@Spy vs @SpyBean

@Spy와 @SpyBean의 차이는 @Mock과 @MockBean의 차이와 같다. AppCtx, BeanFactory에 로딩되어 자동으로 주입되느냐, 명시적으로 @InjectMock을 넣어주어야 하느냐 차이.

@Spy

  • Mock의 경우 완전히 가상 객체여서 실제 소스코가 수행되지 않고 결과에 대한 리턴만 Stub할 수 있다.
  • 이럴때, 일부 메소드는 Stub 하고 또 다른 메소드는 실제 소스가 수행되는 테스트를 구성하고 싶은 경우에 사용하는게 @Spy
  • Mock을 원하는 대상 메소드만 Stub처리하면 다른 메소드는 실제 소스코드가 수행된다.

@SpyBean

  • AppCtx로딩시 BeanFactory에 등록된다.
  • 그래서 자동으로 의존성 주입이 된다.
  • 사용방법은 @Spy와 동일하다.

Junit5

Junit4 vs Junit5 변경사항

JUnit4Junit5역할
@Before/@After@BeforeEach/@AfterEach각 테스트 호출 전/후에 수행
@BeforeClass/@AfterClass@BeforeAll/@AfterAll전체 테스트 시작 전/후에 한 번만 수행
@Ignore@Disabled대상 테스트를 무시하고 수행하지 않는다
@RunWith@ExtendWith✨Junit테스트 러너를 결정한다. (@SpringBootTest / @WebMvcTest에 내장)
@Test(expected = Exception)@Testexpected 가 5에서 사라져서 예외 검증을 별도로 코드에서 해야함

5버젼에서는 일부 어노테이션이 변경된 점이 가장 눈에 띄고,
SpringBootTest가 Runwith를 내장하면서 생략해도 되는점이 좋은 것 같다.
다만, @Test(expected = Exception)옵션이 제외되고 @Test메소드에서 assertion 해야되는 부분은 좀 귀찮아진 것 같다.

profile
공부합시다

0개의 댓글