RestAssured
와 MockMvc
는 테스트를 할 때 자주 사용되는 도구이다.
MockMvc
는 웹 애플리케이션을 애플리케이션 서버에 배포하지 않고도 스프링 MVC의 동작을 재현할 수 있는 라이브러리이다. 보통 @WebMvcTest
와 자주 사용된다. 실제 프로덕션 코드에서 사용되는 디스패처 서블릿을 사용하지 않고, 테스트용 디스패처 서블릿을 생성해 MVC
동작을 테스트한다. @WebMvcTest
를 통해 Presentation Layer Bean
들만 불러온다. 그리고 그 외 Bean
은 Mock
객체를 사용한다.
RestAssured
는 보통 @SpringBootTest
와 자주 사용된다. Rest한 웹 서비스를 검증할 때 사용되며, 보통 전 구간 테스트에 사용된다.
요약하면, MockMvc
는 MVC
동작에 필요한 빈들만을 등록하고, 컨트롤러의 로직만을 테스트할 때 자주 사용된다. RestAssured
는 애플리케이션의 빈들을 전부 등록하고, 전체적인 로직을 테스트할 때 자주 사용된다.
이번 미션을 하면서 RestAssuredMockMvc
라는 것에 대해 알게되었다. RestAssured
는 특별한 설정없이 @WebMvcTest
와 사용되는게 불가능하나, RestAssuredMockMvc
를 사용하면 @WebMvcTest
와 RestAssured
의 기능을 함께 사용할 수 있다.
// 사용 예시
@WebMvcTest(AdminController.class)
class AdmintControllerTest {
@MockBean
private ProductService productService;
@BeforeEach
void setUp() {
RestAssuredMockMvc.standaloneSetup(new AdminController(productService));
}
@Test
void 모든_상품_목록을_가져온다() {
RestAssuredMockMvc.given().log().all()
.when().get("/admin")
.then().log().all()
.status(HttpStatus.OK);
}
}
RestAssuredMockMvc
를 사용한 테스트에서 Circular View path error
가 발생했다. Circular View path error
는 View파일과 @GetMapping
안의 인자(url 주소)가 같아서 나는 에러이다.
아래에서 다시 서술하겠지만, 디폴트 InternalResourceViewResolver
가 뷰리졸버로 사용되면서 발생한 에러이다. 실제 프로덕션 코드에서는 다른 뷰리졸버가 사용되어 에러가 나지 않았지만 RestAssuredMockMvc
테스트에서는 디폴트 InternalResourceViewResolver
가 채택된듯 하다.
@Controller
public class AdminController {
@GetMapping("/admin")
public String path() { // circular view path error 발생
return "admin";
}
}
baeldung - spring-circular-view-path-error
By default, the Spring MVC framework applies the InternalResourceView class as the view resolver. As a result, if the *@GetMapping* value is the same as the view, the request will fail with the Circular View path error.
InternalResourceView
를 ViewResolver
로 적용한다.InternalResourceView
를 ViewResolver
로 사용할 경우, @GetMapping
의 인자와 view의 이름이 같을 때 Circular View path error
가 발생한다.@GetMapping
의 인자와 view의 이름이 다르게 바꾼다.InternalResourceViewResolver
에 view
파일의 prefix와 suffix를 지정한다.view
의 이름을 알아서 다른걸로 바꾸는 ViewResolver
로 바꾼다.@GetMapping
의 인자와 view의 이름이 다르게 바꾼다.@Controller
public class AdminController {
@GetMapping("/admin")
public String path() {
return "admin2";
}
}
간단한 해결 방법이지만, 테스트시 발생한 Circular View path error
를 해결하자고 요구사항&기존의 설계를 변경하는 것은 최악이다! 🙄
InternalResourceViewResolver
에 view
파일의 prefix와 suffix를 지정한다.InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/templates");
viewResolver.setSuffix(".html");
빈으로 등록되는 InternalResourceViewResolver
에 view파일의 접두사와 접미사를 지정한다.
접두사와 접미사를 지정할 경우, view
파일의 이름을 admin
이 아닌 /templates/admin.html
로 인식하기에, url
주소와 다른 이름이라고 받아들여진다.
ViewResolver
를 사용하게 바꾼다.ThymeleafViewResolver
는 view의 이름을 바꿔주는 로직이 있기에 Circular View path error
가 발생하지 않는다. 테스트에 다른 ViewResolver
를 빈으로 등록하면 되지 않을까?
테스트용 Configuration 생성
@TestConfiguration
public class ViewResolverConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver(){
return new ThymeleafViewResolver(); // 추가적인 설정 필요
}
}
Circular View path error가 났던 클래스에 위 Configuration 적용
@WebMvcTest(AdminController.class)
@Import({ViewResolverConfig.class})
public class AdminControllerTest {
@MockBean
ProductService productService;
@BeforeEach
void setUp() {
RestAssuredMockMvc.standaloneSetup(new AdminController(productService));
}
@Test
void test() throws Exception {
RestAssuredMockMvc.given().log().all()
.when().get("/admin")
.then().log().all()
.status(HttpStatus.OK);
}
}
왜 안되지?!
DispatcherServlet
의 initViewResolvers()
메서드를 디버깅으로 확인해봤다.
@WebMvcTest(AdminController.class)
public class AdminControllerTest {
@MockBean
ProductService productService;
@BeforeEach
void setUp() {
RestAssuredMockMvc.standaloneSetup(new AdminController(productService));
}
@Test
void test() throws Exception {
RestAssuredMockMvc.given().log().all()
.when().get("/admin")
.then().log().all()
.status(HttpStatus.OK);
}
}
AdminControllerTest
를 실행시키면 다음과 같은 과정이 수행된다.
DispatcherServlet
의 initViewResolvers()
메서드를 호출해 viewResolver
들을 초기화한다.
참고
이 때,DispatcherServlet
은 실제 어플리케이션에서 사용되는 디스패처 서블릿이 아니라 테스트용 디스패처 서블릿이다. (TestDispatcherServlet.class)
initViewResolvers()
는 ApplicationContext
에서 ViewResolver
타입인 빈들을 DispatcherServlet
의 필드viewResolvers
에 전부 저장한다.
private void initViewResolvers(ApplicationContext context) {
this.viewResolvers = null;
if (this.detectAllViewResolvers) {
Map<String, ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);
...
}
...
}
앞에서 초기화한 DispatcherServlet
을 가지고 MockMvc
객체를 생성한다.
ViewResolver
에ThymeleafViewResolver
가 잘 들어가는 것을 볼 수 있었다.근데...
viewResolvers
를 보니, 직접 빈으로 주입한ThymeleafViewResolver
외에 이미 다른ThymeleafViewResolver
도 들어가 있는 것을 볼 수 있었다.이미
ThymeleafViewResolver
가 빈으로 등록되어 있었는데 왜Circular View path error
가 발생한걸까?
Circular View path error
가 발생한 이유는 바로 알 수 있었다...
나는 위에서 만들어진 MockMvc
객체가 테스트 메서드의 RestAssuredMockMvc
에서도 똑같이 사용될 거라고 생각했지만, 실제로는 테스트 메서드마다 새로운 context
와 MockMvc
를 만들어서 사용하는 것을 볼 수 있었다.
@BeforeEach
내부에서 RestAssuredMockMvc
를 standaloneSetup()
해주고 있기에, 각 테스트 메서드가 실행되기 전에 MockMvc
객체가 새로 생성되고 있었다.
이 때 사용되는 context
는 StubWebApplicationContext
로, RestAssuredMockMvc
에 context
에 대한 정보를 설정해주지 않는다면, 디폴트로 StubWebApplicationContext
가 사용된다.
StubWebApplicationContext
는 말 그대로 임의로 사용되는 객체이기에 viewResolver
도 InternalResourceViewResolver
하나만을 가지고 있다.
즉, 내가 아무리 AdminControllerTest
클래스에서 다른 ViewResolver
빈을 등록해도, 테스트 메서드가 실행될 때는 아예 다른 context
(StubWebApplicationContext
)를 사용하므로 적용되지 않는 것이었다.
그제서야 RestAssuredMockMvc
의 내장 메소드들을 살펴봤다. ㅋㅋ
RestAssuredMockMvc
에는 webAppContextSetup()
이라는 메서드가 있다. 해당 메서드를 사용해서 RestAssuredMockMvc
에서 사용될 WebApplicationContext
를 설정할 수 있다.
@WebMvcTest(AdminController.class)
public class AdminControllerTest {
@MockBean
ProductService productService;
@Autowired
WebApplicationContext webApplicationContext;
@BeforeEach
void setUp() {
RestAssuredMockMvc.standaloneSetup(new AdminController(productService));
RestAssuredMockMvc.webAppContextSetup(webApplicationContext);
}
...
}
주입받은 webAppContext
를 SetUp하면, 테스트 클래스와 테스트 메서드에 같은 WebApplicationContext
가 적용된다.
같은 WebApplicationContext
가 사용되므로, DispatcherServlet
의 viewResolvers
필드가 동일하게 초기화되었다.
WebApplicationContext
에는 InternalResourceViewResolver
외에 다른 뷰리졸버 객체들도 빈으로 등록되어 있기에 Circular View path error
가 발생하지 않았다.
RestAssuredMockMvc
에는 mockMvc()
라는 메서드도 있다. 해당 메서드를 사용해서 RestAssuredMockMvc
에서 사용될 MockMvc
객체를 설정할 수 있다.
@WebMvcTest(AdminController.class)
public class AdminControllerTest {
@MockBean
ProductService productService;
@Autowired
MockMvc mockMvc;
@BeforeEach
void setUp() {
RestAssuredMockMvc.standaloneSetup(new AdminController(productService));
RestAssuredMockMvc.mockMvc(mockMvc);
}
...
}
mockMVc()
를 사용해 주입받은 MockMvc
객체를 등록하면 테스트 클래스와 테스트 메서드에서 같은 MockMvc
객체가 사용된다. 주입받은 MockMvc
객체에는 InternalResourceViewResolver
외에 다른 뷰리졸버 객체들도 빈으로 등록되어 있기에 Circular View path error
가 발생하지 않았다.
우선 두 방법 다 테스트 메서드에 WebApplicationContext
를 적용한다. WebApplicationContext
를 주입받기에 디폴트 InternalResourceViewResolver
외에 다른 뷰리졸버를 가져올 수 있다.
webAppContextSetup()
를 사용하면 TestDispatcherServlet
의 초기화가 테스트 클래스 + 테스터 메서드의 수 만큼 실행된다. 같은 context
를 사용하지만, 테스트 인스턴스마다 다른 TestDispatcherServlet
을 생성한다. 즉, 같은 context
를 참조하는 여러 개의 TestDispatcherServlet
, MockMvc
객체가 생성된다.
mockMvc()
를 사용하면 TestDispatcherServlet
의 초기화가 한 번만 실행된다. 이 때 생성된 TestDispatcherServlet
과 이를 사용한 MockMvc
가 모든 테스트 메서드에서 사용된다.
아직 지식이 부족해서 확답은 못하겠지만, WebApplicationContext
를 등록했을 때 이득인 경우는 모르겠다. 테스트 메서드마다 다른 Configuration
을 적용하면 유용할지도...? 지금 시점에서는 잘 모르겠다. WebApplicationContext()
를 사용하는 경우를 더 조사해봐야할 것 같다.
MockMvcBuilders
를 사용해 테스트 인스턴스에서 사용될 MockMvc
객체를 초기화한다.
@WebMvcTest(AdminController.class)
public class AdminControllerTest {
@MockBean
ProductService productService;
@BeforeEach
void setUp() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/templates");
viewResolver.setSuffix(".html");
RestAssuredMockMvc.standaloneSetup(
MockMvcBuilders.standaloneSetup(new AdminController(productService))
.setViewResolvers(viewResolver)
);
}
이럴 경우, 테스트 메서드마다 MockMvcBuilders
를 사용해 설정한 MockMvc
객체가 등록된다. ApplicationContext
는 StubWebApplicationContext
가 사용되며, MockMvcBuilders.standaloneSetUp()
에서 설정한 빈들이 등록된다.
이쯤 읽으면 RestAssuredMockMvc
를 사용하는 게 좋을까?하는 생각이 든다...
바로 @Autowired
를 사용해서 테스트할 수 있는 MockMvc
에 비해 빈을 관리하는 방법이 복잡한 것만 같다.
각 테스트 도구로 api 테스트를 100번 수행하는 시간을 측정해본 결과, MockMvc
가 RestAssuredMockMvc
보다 실행 속도가 조금 더 빠르기도 하다. 둘 다 @WebMvcTest
와 함께 사용되므로 테스트 클래스가 실행될 때 로드하는 빈의 개수는 110개로 동일하다. 때문에 이 부분에서 유의미한 시간 차이는 발생한 것 같지않고, 어떻게 MockMvc
객체를 생성하느냐, 어떻게 api테스트 메서드를 실행시키냐에서 나는 시간차이인 것 같다. 뇌피셜임. (물론 테스트 메서드마다 context에 등록되는 빈의 개수는 어떻게 설정하느냐에 따라 다르다.)
RestAssuredMockMvc 사용
- @BeforeEach 마다 주입한 WebApplicationContext 로 setUp 해준 결과
- (1) 4sec 268ms, (2) 4sec 786ms, (3) 4sec 273ms
- @BeforeEach 마다 주입한 MockMvc 로 setUp 해준 결과
- (1) 3sec 918ms, (2) 3sec 950ms, (3) 3sec 982ms 3.
- @BeforeEach 마다 MockMvcBuilders 로 setUp 해준 결과
- (1) 7sec 358ms, (2) 8sec 284ms, (3) 8sec 484ms
MockMvc
사용
- 주입받은 MockMvc를 사용한 결과 :
- (1) 2sec 30ms, (2) 1sec 982ms, (3) 1sec 945ms
- @BeforeEach 마다 MockMvc 설정 해준 결과
- (1) 5sec 707ms, (2) 6sec 42ms, (3) 5sec 644ms
결과적으로, 두 개의 테스트 도구 중 어떤 것을 사용할지는 취향의 문제가 될 것 같다.
조금이라도 더 빠르게 테스트를 실행하고싶다면 MockMvc
를, RestAssured
의 함수를 사용해 MVC
를 테스트하고 싶다면 RestAssuredMockMvc
를 사용하면 좋을 것 같아. 실제로 응답에 대한 body
를 검증하기에는 RestAssuredMockMvc
의 메서드를 사용하는게 더 편리한 것 같다.
RestAssuredMockMvc
는 별다른 설정이 없다면 테스트 인스턴스마다 StubWebApplicationContext
를 사용한다.
StubWebApplicationContext
는 뷰리졸버로 InternalResourceViewResolver
하나만을 가지고 있다. 만약 다른 뷰리졸버를 사용하고 싶다면, MockMvcBuidlers
를 사용해 viewResolver
객체를 설정해주는 게 좋을 것 같다.
Stub
객체인 만큼 뷰리졸버외에도 등록되지 않는 빈들이 많이 있을 것 같다. RestAssuredMockMVc
를 사용하다가 원하는 빈이 없어 에러가 발생하면 RestAssuredMockMVc
의 내장 함수인 webApplicationContestSetUp()
또는 mockMvc()
를 사용하는 것을 고려해보자.
만약 MockMvcBuilders
의 setter
에 내가 설정하고싶은 빈과 관련된 setter
가 있다면 이를 사용해도 좋을 것 같다.