공부함

테스트 환경 (@SpringBootTest와 @WebMvcTest) 본문

테스트

테스트 환경 (@SpringBootTest와 @WebMvcTest)

찌땀 2024. 9. 8. 19:08

지금까지 테스트 코드를 짜면서 테스트 코드 자체에만 집중했지 테스트 환경에는 크게 관심 없이 남들이 하는대로 가져다 썼던 것 같다. 그래서 이번에는 테스트 환경설정에 대해 알아보려고 한다.

@Mock VS @MockBean

먼저 mock 관련해 간단하게 알고 넘어가자.

@Mock

  • Mock 객체를 생성해 사용한다.
  • 스프링 컨텍스트와 무관하다.
  • 단위 테스트에 사용

@MockBean

  • Mock 객체를 생성해 스프링 컨텍스트에 등록한다.
  • 통합 테스트에 사용

@SpringBootTest

@SpringBootTest는 전체 애플리케이션 컨텍스트를 로드한다. 즉 모든 빈을 로드한다.
@SpringBootTest는 이러한 상황에서 사용하면 좋다.

  1. 전체 애플리케이션 컨텍스트가 필요할 때
  2. 통합 테스트를 하고 싶을 때 (모든 빈, 서비스, 컨트롤러 등)
  3. 실제 db 연결이나 외부 시스템과의 통합 테스트가 필요할 때
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class AuthControllerTest {

    @Autowired
    private WebTestClient webTestClient;

    @Autowired
    private JwtTokenProvider jwtTokenProvider;

    @MockBean
    private MemberRepository memberRepository;


    @DisplayName("토큰을 재발급 받을 수 있다")
    @Test
    void 토큰_재발급성공() {
        // given
        Member 지담 = MemberFixture.builder().id(1L).build();
        String refreshToken = jwtTokenProvider.createRefreshToken(지담.getId());

        when(memberRepository.findById(지담.getId())).thenReturn(Optional.of(지담));
        when(memberRepository.findByRefreshToken(refreshToken)).thenReturn(Optional.of(지담));

        webTestClient.post().uri("/api/v1/reissue/kakao")
                .cookie("refreshToken", refreshToken)
                .exchange()
                .expectStatus().isFound()
                .expectHeader().value("accessToken", accessToken -> {
                    assertThat(jwtTokenProvider.getMemberId(accessToken)).isEqualTo(지담.getId());
                });
    }
}

@SpringBootTest는 애플리케이션 컨텍스트를 로드한다. 따라서 @Component로 등록한 JwtTokenProvider를 @Autowired로 필드주입 받을 수 있다. 왜냐하면 스프링 컨테이너에 빈으로 등록되어야만 자동주입 받을 수 있기 때문이다. 만약 위 테스트에서 @SpringBootTest를 주석처리 한다면 어떻게 될까?

애플리케이션 컨텍스트를 로드하지 않기 때문에 @Autowired로 빈 JwtTokenProvider를 불러올 수 없어 NPE가 터졌다.

 

@SpringBootTest 환경에서 mock을 하고 싶다면 @MockBean을 사용해서 애플리케이션 컨텍스트에 mocking한 빈을 등록해 줘야 한다. 위 코드에서 @MockBean으로 MemberRepository mock 객체를 생성하고 빈으로 등록해 사용하는 것을 확인할 수 있다. 

 

@SpringBootTest 애노테이션의 주요 속성

@SpringBootTest(
        properties = {"property.password=1234"},
        classes = {MyTestConfiguration.class},
        webEnvironment = WebEnvironment.RANDOM_PORT
)
public class SampleTest {

    @Value("${property.password}")
    private String testPW;
}

@SpringBootTest에서 쓰이는 속성은 주로 3가지다.

properties

테스트에서 쓰일 property 값을 지정할 수 있다. @Value로 값을 사용할 수 있다. 

classes 

가져올 애플리케이션 컨텍스트를 지정한다. 기본적으로는 @SpringBootConfiguration을 찾아 로드한다. 

관련해서는 더보기를 참고하자.

더보기

스프링부트로 프로젝트를 생성하면 @SpringBootApplication이 달린 클래스가 생성되고 이 클래스를 통해 앱을 실행한다. 

@SpringBootApplication
@EnableJpaAuditing
public class RednoseApplication {

    public static void main(String[] args) {
        SpringApplication.run(RednoseApplication.class, args);
    }

}

@SpringBootApplication을 타고 들어가보면 

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
        .... 이하생략

@SpringBootConfiguration을 포함하고 있는 것을 확인할 수 있다. 

@SpringBootConfiguration@Configuration의 일종이지만 특수한 버전으로 하나만 존재할 수 있다.  

https://coding-zzang.tistory.com/43

 

[스프링/Spring] @SpringBootApplication 어노테이션을 완벽하게 파헤쳐보자

0. 목차 1. 서론 2. 본론 @SpringBootConfiguration @ComponentScan @EnableAutoConfiguration 3. 정리 서론 스프링부트 프로젝트를 생성을 하게되면, 기본 application클래스가 생성이된다. 해당 클래스를 잘 살펴보면 @Sp

coding-zzang.tistory.com

 

webEnvironment

실행 시 웹 환경을 설정하는 것으로 예시에서는 랜덤 포트값을 사용하고 있다.

 

@SpringBootTest는 모든 빈을 탐색, 로드하기 때문에 통합 테스트에 적합하다. 특정 레이어만 테스트하고 싶은 경우에는 테스트가 불필요하게 무거워진다. 

@WebMvcTest

주로 컨트롤러 테스트에 사용되며 테스트 대상과 관련된 웹 계층 빈들만 로드되므로 다른 컴포넌트들은 mocking 해야 한다.

컨트롤러 레이어만 테스트하는 가벼운 테스트에 사용한다. 

 

@WebMvcTest(HelloController.class)
class HelloControllerTest3 {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {
        MvcResult mvcResult = mockMvc.perform(get("/hello/3"))
                .andExpect(status().isOk())
                .andReturn();
        MockHttpServletResponse response = mvcResult.getResponse();
        Assertions.assertThat(response.getContentAsString()).isEqualTo("hellohellohello");
    }
}

 

이렇게 테스트코드를 작성한다면 에러가 발생한다. 

Error creating bean with name 'helloController' defined in file [C:\project\demo\demo\build\classes\java\main\com\example\demo\controller\HelloController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'com.example.demo.service.HelloService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

HelloController 빈 생성에 실패한다. 왜냐하면 HelloService를 로드할 수 없기 때문이다. 

 

@WebMvcTest(HelloController.class)
class HelloControllerTest3 {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private HelloService helloService;

    @Test
    public void helloTest() throws Exception {
        when(helloService.hello(3)).thenReturn("hellohellohello");

        MvcResult mvcResult = mockMvc.perform(get("/hello/3"))
                .andExpect(status().isOk())
                .andReturn();
        MockHttpServletResponse response = mvcResult.getResponse();
        Assertions.assertThat(response.getContentAsString()).isEqualTo("hellohellohello");
    }
}

이렇게 서비스 레이어는 mocking을 해줘야 한다. 

 

MockMvc

컨트롤러 테스트 시 사용할 수 있는 MockMvc 환경설정에 대해서도 간단히 알아보자. 

 

MockMvcBuilders.standaloneSetup()

@SpringBootTest
class HelloControllerTest {
    private MockMvc mockMvc;

    @Autowired
    private HelloController helloController;

    @BeforeEach
    void setUp() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(helloController).build();
    }

    @Test
    public void helloTest() throws Exception {
        mockMvc.perform(get("/hello/3"))
                .andExpect(status().isOk());
    }
}

 

standaloneSetup(helloController) 와 같이 MockMvc를 초기화하면 HelloController만 테스트하기 위한 환경이 된다. 애플리케이션 컨텍스트를 전부 로드하지 않고  @ControllerAdvice, @RestController, @RequestMapping  등 만 로드하며 @Service @Repository 는 로드하지 않는다. 

위의 예시에서는 @SpringBootTest를 사용하고 있어서 어차피 전체 애플리케이션 컨텍스트가 로드된다. 그렇기 때문에 @AutoWired로 HelloController를 주입 할 수 있다. 

class HelloControllerTest2 {

    private MockMvc mockMvc;
    
    @BeforeEach
    void setUp() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new HelloController(new HelloService())).build();
    }

    @Test
    public void helloTest() throws Exception {
        MvcResult mvcResult = mockMvc.perform(get("/hello/3"))
                .andExpect(status().isOk())
                .andReturn();
        MockHttpServletResponse response = mvcResult.getResponse();
        Assertions.assertThat(response.getContentAsString()).isEqualTo("hellohellohello");
    }
}

정말 필요한 객체만 사용하고 싶다면 이렇게 작성할 수 있겠다. 

 

@AutoConfigureMockMvc + @Autowired 

@SpringBootTest
@AutoConfigureMockMvc
class HelloControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void helloTest() throws Exception {
        mockMvc.perform(get("/hello/3"))
                .andExpect(status().isOk());
    }

    @Test
    public void byeTest() throws Exception {
        mockMvc.perform(get("/bye/3"))
                .andExpect(status().isOk());
    }
}

@AutoConfigureMockMvc는 컨트롤러 뿐만 아니라 애플리케이션의 모든 빈을 로드한다. 

특정 컨트롤러를 지정해 MockMvc를 초기화 한 것이 아니므로 ByeController에 대한 테스트도 성공하는 걸 확인 할 수 있다.  

 

@ExtendWith(SpringExtension.class)

@ExtendWith(SpringExtension.class)는 테스트 실행 시 JUnit과 스프링 컨텍스트를 통합한다. 스프링 기능인 의존성 주입, 트랜잭션 관리 등을 사용할 수 있게 한다. 애플리케이션 컨텍스트를 직접 로드하는 것은 아니다. 애플리케이션 컨텍스트 로딩은 @BootStrapWith@ContextConfiguration 등이 수행한다.  

JUnit 5에서 사용되며 JUnit4에서는 @RunWith(SpringRunner.class)가 그 기능을 한다. 

@SpringBootTest나 @WebMvcTest@ExtendWith(SpringExtension.class)가 포함된다. 그렇기 때문에 @Autowired를 사용할 수 있고 @MockBean으로 생성 후 등록한 mock 빈도 사용할 수 있는 것이다.  

 

@BootstrapWith

@BootstrapWith는 테스트 컨텍스트의 부트스트래핑 전략을 지정하는데 사용되는 애노테이션이다. 테스트 컨텍스트는 테스트 시 사용되는 애플리케이션 컨텍스트를 의미한다.

 

이 애노테이션을 통해 TestContextBootStrapper의 구현체를 지정해 스프링 테스트의 환경설정을 할 수 있다. TestContextBootStrapper는 테스트 컨텍스트를 초기화, 관리하는 역할을 하는 인터페이스다. 사용자가 직접 구현할 수 있지만, 일반적으로 Spring에서 제공하는 기본 구현체를 사용하거나 확장한다. @BootstrapWith를 사용하지 않으면 스프링이 DefaultTestContextBootstrapper나 WebTestContextBootstrapper중 선택해 테스트 컨텍스트를 구성한다. 

 

@SpringBootTest@BootStrapWith(SpringBootTestContextBootstraper.class)를 포함한다. SpringBootTestContextBootstrapper는 애플리케이션 컨텍스트 전체를 로드하여 스프링부트 통합 테스트가 가능하도록 하는 부트스트래퍼이다. 실제 db나 외부 api와 통합된 상태로 테스트 할 수 있다. 

 

@WebMvcTest는 @BootstrapWith(WebMvcContextBootstrapper.class)를 포함한다. WebMvcContextBootstrapper.class는 mvc테스트를 위한 계층의 빈만 로드하는 부트스트래퍼이다. 여기서 mvc테스트를 위한 계층의 빈은 @Controller, @ControllerAdvice, Filter, Interceptor, Validator, Formatter  등을 의미하며 @Service, @Repository 빈 등은 로드되지 않기 때문에 mock 하는 것이 일반적이다. 

 

SpringBootTestContextBootstrappeWebMvcContextBootstrapper 모두 spring boot의 자동 설정 메커니즘이 필요하므로 non-springboot 환경에서는 사용할 수 없다. 

non-SpringBoot 환경에서 테스트 구성하기 

사실 내가 이 글을 쓰게 된 가장 주된 이유다. non-springboot 환경에서 테스트 환경을 구성하려니 springboot 환경에서 편하게 사용하던 설정들을 사용할 수 없었고 그 이유가 궁금했다. 앞에서 말했듯이 @SpringBootTest나 @WebMvcTest 는 non-springboot 환경에서 사용할 수 없다. 

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = {})
@WebAppConfiguration
class HelloControllerTest4 {

    @Autowired
    WebApplicationContext wac;
    
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
    }

mockMvc를 활용한 컨트롤러를 non-spingboot 환경에서 테스트하는 예시이다.

@ContextConfiguration으로 locations의 위치에 해당하는 설정파일들을 직접 불러온다. 설정파일에 등록된 빈만 로드한다. @WebAppConfiguration은 mvc 테스트 곤련된 빈들을 로드한다. 위에서 언급했듯이 이것은 @ControllerAdvice, Filter, Interceptor, Validator, Formatter  등을 의미하며 @Service, @Repository 빈 등은 로드되지 않는다. @ExtendWith(SpringExtension.class)를 사용하기 때문에 JUnit과 스프링 컨텍스트가 통합되어 @Autowired 와 같은 의존성 주입 등의 스프링 기능을 사용할 수 있다.  추가로 mockMvc는 WebApplicationContext를 활용해 초기화해주고 있다. 

 

꼭 non-springboot 상황이 아니어도 원하는 빈을 로드하고 싶다면 @ContextConfiguration을 활용할 수 있겠다. 

정리

@SpringBootTest

  • 애플리케이션 컨텍스트 모든 빈을 로드한다. 
  • 통합 테스트에 적합하다. 

@WebMvcTest

  • 관련 계층 빈만 로드한다. 
  • 단위 테스트에 적합하다. (컨트롤러 계층)

@ExtendWith(SpringExtension.class)

  • 테스트 환경에서 스프링 컨텍스트를 로드해 빈을 주입받을 수 있게 한다. 
  • 위 두 애노테이션에 포함되어 있다.

MockMvc

  • 컨트롤러 테스트에 사용된다.
  • @SpringBootTest@WebMvcTest와 함께 사용한다면 @Autowired로 주입받을 수 있다.
  • 통합 테스트라면 @SpringBootTest  + @Autowired MockMvc 조합을 사용하자
  • 단위 테스트라면(컨트롤러) @WebMvcTest  + @Autowired MockMvc 조합을 사용하자

 

출처

https://medium.com/@hgbaek1128/springboottest%EC%99%80-webmvctest-54442cf9411d
https://m.blog.naver.com/seek316/222385186655
https://m.blog.naver.com/sosow0212/223076265261

'테스트' 카테고리의 다른 글

@ParameterizedTest를 사용해보자  (0) 2024.10.31
테스트 더미데이터 삽입 방법  (2) 2024.10.21