RestAssuredMockMvc 사용 후기

hongo·2023년 5월 2일
0
post-thumbnail

RestAssuredMockMvc 사용 후기

RestAssured와 MockMvc

RestAssuredMockMvc는 테스트를 할 때 자주 사용되는 도구이다.

MockMvc는 웹 애플리케이션을 애플리케이션 서버에 배포하지 않고도 스프링 MVC의 동작을 재현할 수 있는 라이브러리이다. 보통 @WebMvcTest와 자주 사용된다. 실제 프로덕션 코드에서 사용되는 디스패처 서블릿을 사용하지 않고, 테스트용 디스패처 서블릿을 생성해 MVC 동작을 테스트한다. @WebMvcTest를 통해 Presentation Layer Bean들만 불러온다. 그리고 그 외 BeanMock 객체를 사용한다.

RestAssured는 보통 @SpringBootTest와 자주 사용된다. Rest한 웹 서비스를 검증할 때 사용되며, 보통 전 구간 테스트에 사용된다.

요약하면, MockMvcMVC동작에 필요한 빈들만을 등록하고, 컨트롤러의 로직만을 테스트할 때 자주 사용된다. RestAssured는 애플리케이션의 빈들을 전부 등록하고, 전체적인 로직을 테스트할 때 자주 사용된다.

RestAssuredMockMvc

이번 미션을 하면서 RestAssuredMockMvc라는 것에 대해 알게되었다. RestAssured는 특별한 설정없이 @WebMvcTest와 사용되는게 불가능하나, RestAssuredMockMvc를 사용하면 @WebMvcTestRestAssured의 기능을 함께 사용할 수 있다.

// 사용 예시

@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);
    }
}

Circular View path error

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.

  • 기본적으로 Spring Mvc framework는 InternalResourceViewViewResolver로 적용한다.
  • InternalResourceViewViewResolver로 사용할 경우, @GetMapping의 인자와 view의 이름이 같을 때 Circular View path error가 발생한다.

Circular View path error 해결 방법

  1. @GetMapping의 인자와 view의 이름이 다르게 바꾼다.
  2. InternalResourceViewResolverview파일의 prefix와 suffix를 지정한다.
  3. view의 이름을 알아서 다른걸로 바꾸는 ViewResolver로 바꾼다.

@GetMapping의 인자와 view의 이름이 다르게 바꾼다.

@Controller
public class AdminController {
  @GetMapping("/admin")
  public String path() {
    return "admin2";
  }
}

간단한 해결 방법이지만, 테스트시 발생한 Circular View path error를 해결하자고 요구사항&기존의 설계를 변경하는 것은 최악이다! 🙄

InternalResourceViewResolverview파일의 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를 빈으로 등록하면 되지 않을까?

ViewResolver를 ApplicationContext에 추가

테스트용 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);
    }
    
}

실행 결과 : 안됨

왜 안되지?!

DispatcherServletinitViewResolvers()메서드를 디버깅으로 확인해봤다.

@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를 실행시키면 다음과 같은 과정이 수행된다.

1. DispatcherServlet에서 뷰리졸버 초기화

DispatcherServletinitViewResolvers()메서드를 호출해 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);
    		
                ...
    		}
        ...
    }

2. MockMvc 객체 생성

앞에서 초기화한 DispatcherServlet을 가지고 MockMvc객체를 생성한다.

ViewResolverThymeleafViewResolver 가 잘 들어가는 것을 볼 수 있었다.

근데... viewResolvers를 보니, 직접 빈으로 주입한 ThymeleafViewResolver외에 이미 다른 ThymeleafViewResolver도 들어가 있는 것을 볼 수 있었다.

이미 ThymeleafViewResolver가 빈으로 등록되어 있었는데 왜 Circular View path error가 발생한걸까?

각 테스트 메서드가 실행될 때

Circular View path error가 발생한 이유는 바로 알 수 있었다...

나는 위에서 만들어진 MockMvc객체가 테스트 메서드의 RestAssuredMockMvc에서도 똑같이 사용될 거라고 생각했지만, 실제로는 테스트 메서드마다 새로운 contextMockMvc를 만들어서 사용하는 것을 볼 수 있었다.

@BeforeEach내부에서 RestAssuredMockMvcstandaloneSetup() 해주고 있기에, 각 테스트 메서드가 실행되기 전에 MockMvc객체가 새로 생성되고 있었다.

이 때 사용되는 contextStubWebApplicationContext로, RestAssuredMockMvccontext에 대한 정보를 설정해주지 않는다면, 디폴트로 StubWebApplicationContext가 사용된다.

StubWebApplicationContext는 말 그대로 임의로 사용되는 객체이기에 viewResolverInternalResourceViewResolver 하나만을 가지고 있다.

즉, 내가 아무리 AdminControllerTest 클래스에서 다른 ViewResolver빈을 등록해도, 테스트 메서드가 실행될 때는 아예 다른 context(StubWebApplicationContext)를 사용하므로 적용되지 않는 것이었다.

webAppContextSetup()

그제서야 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가 사용되므로, DispatcherServletviewResolvers 필드가 동일하게 초기화되었다.

WebApplicationContext에는 InternalResourceViewResolver외에 다른 뷰리졸버 객체들도 빈으로 등록되어 있기에 Circular View path error 가 발생하지 않았다.

mockMvc()

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 가 발생하지 않았다.

webAppContextSetup() vs mockMvc()

  • 공통점

우선 두 방법 다 테스트 메서드에 WebApplicationContext를 적용한다. WebApplicationContext를 주입받기에 디폴트 InternalResourceViewResolver외에 다른 뷰리졸버를 가져올 수 있다.

  • 차이점

webAppContextSetup()를 사용하면 TestDispatcherServlet의 초기화가 테스트 클래스 + 테스터 메서드의 수 만큼 실행된다. 같은 context를 사용하지만, 테스트 인스턴스마다 다른 TestDispatcherServlet을 생성한다. 즉, 같은 context를 참조하는 여러 개의 TestDispatcherServlet, MockMvc객체가 생성된다.

mockMvc()를 사용하면 TestDispatcherServlet의 초기화가 한 번만 실행된다. 이 때 생성된 TestDispatcherServlet과 이를 사용한 MockMvc가 모든 테스트 메서드에서 사용된다.

아직 지식이 부족해서 확답은 못하겠지만, WebApplicationContext를 등록했을 때 이득인 경우는 모르겠다. 테스트 메서드마다 다른 Configuration을 적용하면 유용할지도...? 지금 시점에서는 잘 모르겠다. WebApplicationContext()를 사용하는 경우를 더 조사해봐야할 것 같다.

MockMvcBuilders 사용

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객체가 등록된다. ApplicationContextStubWebApplicationContext가 사용되며, MockMvcBuilders.standaloneSetUp()에서 설정한 빈들이 등록된다.

MockMvc vs RestAssuredMockMvc?

이쯤 읽으면 RestAssuredMockMvc를 사용하는 게 좋을까?하는 생각이 든다...

바로 @Autowired를 사용해서 테스트할 수 있는 MockMvc에 비해 빈을 관리하는 방법이 복잡한 것만 같다.

각 테스트 도구로 api 테스트를 100번 수행하는 시간을 측정해본 결과, MockMvcRestAssuredMockMvc보다 실행 속도가 조금 더 빠르기도 하다. 둘 다 @WebMvcTest와 함께 사용되므로 테스트 클래스가 실행될 때 로드하는 빈의 개수는 110개로 동일하다. 때문에 이 부분에서 유의미한 시간 차이는 발생한 것 같지않고, 어떻게 MockMvc객체를 생성하느냐, 어떻게 api테스트 메서드를 실행시키냐에서 나는 시간차이인 것 같다. 뇌피셜임. (물론 테스트 메서드마다 context에 등록되는 빈의 개수는 어떻게 설정하느냐에 따라 다르다.)

RestAssuredMockMvc 사용

  1. @BeforeEach 마다 주입한 WebApplicationContext 로 setUp 해준 결과
    • (1) 4sec 268ms, (2) 4sec 786ms, (3) 4sec 273ms
  2. @BeforeEach 마다 주입한 MockMvc 로 setUp 해준 결과
    • (1) 3sec 918ms, (2) 3sec 950ms, (3) 3sec 982ms 3.
  3. @BeforeEach 마다 MockMvcBuilders 로 setUp 해준 결과
    • (1) 7sec 358ms, (2) 8sec 284ms, (3) 8sec 484ms

MockMvc 사용

  1. 주입받은 MockMvc를 사용한 결과 :
    • (1) 2sec 30ms, (2) 1sec 982ms, (3) 1sec 945ms
  2. @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()를 사용하는 것을 고려해보자.

만약 MockMvcBuilderssetter에 내가 설정하고싶은 빈과 관련된 setter가 있다면 이를 사용해도 좋을 것 같다.

0개의 댓글