공부함
테스트 환경 (@SpringBootTest와 @WebMvcTest) 본문
지금까지 테스트 코드를 짜면서 테스트 코드 자체에만 집중했지 테스트 환경에는 크게 관심 없이 남들이 하는대로 가져다 썼던 것 같다. 그래서 이번에는 테스트 환경설정에 대해 알아보려고 한다.
@Mock VS @MockBean
먼저 mock 관련해 간단하게 알고 넘어가자.
@Mock
- Mock 객체를 생성해 사용한다.
- 스프링 컨텍스트와 무관하다.
- 단위 테스트에 사용
@MockBean
- Mock 객체를 생성해 스프링 컨텍스트에 등록한다.
- 통합 테스트에 사용
@SpringBootTest
@SpringBootTest
는 전체 애플리케이션 컨텍스트를 로드한다. 즉 모든 빈을 로드한다.@SpringBootTest
는 이러한 상황에서 사용하면 좋다.
- 전체 애플리케이션 컨텍스트가 필요할 때
- 통합 테스트를 하고 싶을 때 (모든 빈, 서비스, 컨트롤러 등)
- 실제 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
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 하는 것이 일반적이다.
SpringBootTestContextBootstrappe와 WebMvcContextBootstrapper 모두 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 |