[spring-projects/spring-boot]在 MockMvcAutoConfiguration 中公开 TestDispatcherServlet bean

2024-04-17 145 views
9

@AutoConfigureMockMvc我在测试中使用时遇到了不便。我有一个控制器,它接受多部分请求并将每个不同的部分委托给一个Dispatcherservlet实例,该实例自动连接到该控制器中 - 只是一种批处理端点实现。一切都运行良好,直到我尝试通过MockMvc@AutoConfigureMockMvc)对其进行测试。

事实证明,MockMvcbean 是在单个隔离工厂方法中创建的,不与应用程序上下文交互(调度程序 servlet 实例作为本地变量创建,并直接传递给MockMvc构造函数,而不是在应用程序上下文中查找)。这会阻止我的控制器被实例化,因为DispatcherServlet在应用程序上下文中找不到所需的 ()。我通过实现一个Configuration类来克服这个问题,该类创建该类的一个 beanTestDispatcherServlet并将其注入到一个MockMvcbean 中。如果这种行为是开箱即用的,那就太好了。提前致谢。

回答

0

我通过实现一个 Configuration 类来克服这个问题,该类创建 TestDispatcherServlet 类的 bean 并将其注入到 MockMvc bean 中。

@mpryahin 这是否意味着您最终会得到两个调度程序 servlet,一个直接传递给MockMvc构造函数,另一个作为 bean 创建?

您能否提供一个代码片段来帮助我们更好地理解用例?

5

不,事实并非如此。

我在这里发现的用例的灵感 是我有一个自动装配DispatcherServlet实例的控制器类。该控制器的目的是接受批量请求,每个请求包含一个或多个自给自足的 http 请求(具有自己的标头、正文、http 方法、url 等),然后通过自动连线DispatcherSevlet实例分派每个子请求,然后接收响应,将它们组装成批响应并将其发送回客户端。

需要注意的要点是我的控制器自动装配了一个DispatcherServletbean。

我尝试使用标准测试我的批处理控制器@AutoConfigureMockMvc,但由于测试配置没有暴露任何bean而失败,该配置由@AutoConfigureMockMvc 您可以看到MockMvc实例是使用预先配置的DispatcherServlet实例创建的,并且该实例无法从spring上下文访问。

我还看到这个 servlet 可以由定制者定制,但它并没有解决我在 spring 上下文中没有 bean 的主要问题。

之后,我模仿了 Spring 提供的测试配置类,唯一的区别是 - 我将实例公开DispatcherServlet为一个 bean,如下所示:

/**
 * A custom {@link MockMvc} configuration class exposes {@link DispatcherServlet} instance as a spring bean unlike the default
 * approach of creating a {@link MockMvc} instance via {@link MockMvcBuilderSupport#createMockMvc(Filter[], MockServletConfig, WebApplicationContext, RequestBuilder, List, List, List)}
 * method that inlines the {@link DispatcherServlet} instance as a method variable.
 *
 * @see org.springframework.boot.test.autoconfigure.web.servlet.MockMvcAutoConfiguration
 * @see org.springframework.test.web.servlet.MockMvcBuilderSupport
 */

@AutoConfigureWebMvc
public class MockMvcAutoConfiguration {

    @Autowired
    private WebApplicationContext webAppContext;

    @Bean
    public DispatcherServlet dispatcherServlet() throws Exception {
        WebApplicationContext wac = initWebAppContext();
        ServletContext servletContext = wac.getServletContext();
        MockServletConfig mockServletConfig = new MockServletConfig(servletContext);
        TestDispatcherServlet dispatcherServlet = new TestDispatcherServlet(wac);

        dispatcherServlet.init(mockServletConfig);
        return dispatcherServlet;
    }

    @Bean
    public MockMvc mockMvc(DispatcherServlet dispatcherServlet) {
        ServletContext servletContext = webAppContext.getServletContext();
        MockMvc mockMvc = new MockMvc((TestDispatcherServlet) dispatcherServlet, new Filter[0], servletContext);
        mockMvc.setDefaultRequest(MockMvcRequestBuilders.get("/"));
        mockMvc.setGlobalResultMatchers(Collections.emptyList());
        mockMvc.setGlobalResultHandlers(Collections.emptyList());
        return mockMvc;
    }

    private WebApplicationContext initWebAppContext() {
        ServletContext servletContext = webAppContext.getServletContext();
        ApplicationContext rootWac = WebApplicationContextUtils.getWebApplicationContext(servletContext);
        if (rootWac == null) {
            rootWac = webAppContext;
            ApplicationContext parent = webAppContext.getParent();
            while (parent != null) {
                if (parent instanceof WebApplicationContext && !(parent.getParent() instanceof WebApplicationContext)) {
                    rootWac = parent;
                    break;
                }
                parent = parent.getParent();
            }
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, rootWac);
        }
        return webAppContext;
    }
}

如果 Spring 将实例公开为 bean,那就太好了,DispatcherServlet这样用户就不需要像我那样实现自己的配置来解决类似的任务。

4

new MockMvc(...)@mpryahin当构造函数是包私有时如何调用?该类TestDispatcherServlet也是包私有的。

8

@philwebb我必须将自定义配置类放入正确的包中才能实例化它。这是我想询问的另一个不便之处,但决定推迟,希望最初的请求最终能够得到解决。

8

@mpryahin 谢谢!这就是我们所缺少的信息。我们自己无法做到这一点,因此我们需要更改框架来支持这一点。

4

@rstoyanchev 你对此有何看法。如果我们可以插入我们自己的调度程序 servlet,那就太好了,MockMvc但这意味着对构建器的更改以及需要公开TestDispatcherServlet

另一个侵入性较小的选项可能是添加一个 getter 来MockMvc返回DispatcherServlet实例。这将使我们能够将其注册为 bean。

0

这是一个非常具体的场景,MockMvc 并不是为之设计的。我并不是说它有什么问题,也不是说它不应该支持,只是说它没有被考虑过。我仍然缺少一些关于其工作原理的细节。

例如,在 MockMvc 中,TestDispatcherServlet存储单个请求属性来存储MvcResult依次用于期望的 。在这种逻辑上有多个结果的场景中,它是如何工作的,以及如何针对响应的特定部分声明期望?

每个子调度(即每个批处理请求/部分)是否有自己的请求和响应,以及如何将多个响应聚合回单个多部分响应?

1

@rstoyanchev 让我先澄清一下实现细节,让一切都清楚。

我有一个控制器,它消耗BatchRequest(稍后详细说明)HttpServletRequestHttpServletResponse实例:

@PostMapping(path = "${cleverdata.dmpkit.spring.web.batch.endpoint:/batch}")
public BatchResponse batch(
    @RequestBody
    BatchRequest batchRequest, HttpServletRequest servletRequest,
    HttpServletResponse servletResponse) throws Exception {
    return batchRequestService.process(batchRequest, servletRequest, servletResponse);
}

BatchRequest- 是一个由子请求组成的 pojo,正如我之前所悲伤的那样,这些子请求是具有以下字段的自给自足的请求:

HttpHeaders headers
byte[] body
String httpMethod
String url

该实现迭代子请求对象,使每个子请求对象适应接口HttpServletRequest并调用该DispatcherServlet.service(HttpServletRequest, HttpServletResponse)方法。之后,根据HttpServletResponse传递给方法的相应对象构建子响应对象 DispatcherServlet.service()。所有子响应对象都聚合到 BatchResponse pojo 中并返回给客户端。简化版本如下所示:


public BatchResponse process( 
    BatchRequest request, HttpServletRequest servletRequest,
    HttpServletResponse servletResponse) {

    BatchResponseBuilder batchResponseBuilder = BatchResponse.builder();
    for (BatchRequest.Part requestPart : parts) {

        BatchHttpServletRequest requestWrapper = new BatchHttpServletRequest(
            servletRequest, requestPart
        );
        BatchHttpServletResponse responseWrapper = new BatchHttpServletResponse(servletResponse);

        servlet.service(requestWrapper, responseWrapper);

        BatchResponse.Part responsePart = BatchResponse.Part.builder()
            .status(HttpStatus.valueOf(responseWrapper.getStatus()))
            .body(responseWrapper.getContent())
            .headers(responseWrapper.getHeaderObject())
            .build();
        batchResponseBuilder.part(responsePart);
    }
    return batchResponseBuilder.build();
}

BatchRequest和对象使用符合rfc2046 的BatchResponse自定义消息转换器进行序列化/反序列化。

因此,从MockMvc角度来看,没有任何变化,它的工作方式与以前相同,仅保留一个 MvcResult根据预期进行评估的实例。

如果我们有机会将一个TestDispatcherSevlet实例注册为 spring bean,那就太棒了,我相信我的用例并不那么具体,只是利用 spring 抽象的常见方法(DispatcherServlet

谢谢。

9

感谢@mpryahin 提供额外的细节!该requestPart变量未在示例中使用,可能是拼写错误,我猜您打算将其传递到BatchHttpServletRequest?

ContentResultMatchers鉴于响应是多部分内容,可能无法直接使用内置函数,因为它们处理完整的响应内容?由于这个想法是让子请求不知道批处理,因此您是否考虑过直接对它们进行一一测试,即使用 URL、HTTP 方法和可能的批处理请求的正文?这使得使用内置函数ContentResultMatchers来验证响应成为可能。单独测试批处理层的行为,该行为在任何情况下都是通用的,并且可以模拟批处理子响应。

从建议的选项中,我会选择侵入性最小的选项,即公开一个 getter 来MockMvc补充DispatcherServletCustomizer。我们只需解释一下,提供对DispatcherServlet底层实例的访问MockMvc仅适用于某些组件恰好DispatcherServlet在运行时委托给它并因此需要注入它的情况。更多内容将不会得到明确支持。

6

@rstoyanchev 感谢您的快速回复!

示例中未使用 requestPart 变量,可能是拼写错误,我猜您打算将其传递到 BatchHttpServletRequest 中?

你说得对,很抱歉打错字了,我已经改正了。

考虑到响应是多部分内容,内置的 ContentResultMatchers 可能无法直接使用,因为它们处理完整的响应内容?

除非我必须匹配请求正文,否则所有这些都工作得很好,在这种情况下,将使用自定义匹配器来匹配MockHttpServletResponse对象。

由于这个想法是让子请求不知道批处理,因此您是否考虑过对它们进行一一测试......

其想法Batch Controller是,它被实现为一个由 Spring Boot 自动配置机制支持的独特库,以便任何 RESTful 服务只需将此库包含到其依赖项中,并免费获得批处理端点!这就是为什么我没有能力一一测试底层控制器方法。

单独测试批处理层的行为,...

完全同意,但我想要一种集成测试,以确保实现与 Spring 一起按预期工作DesipatcherServlet

谢谢,最诚挚的问候,迈克。

8

除非我必须匹配请求正文,否则它们都工作得很好

您是指响应主体吗?响应具有多部分内容,因此我不希望任何 ContentResultMatchers 正常工作,除非仅给出单个部分的内容。

我创建了https://jira.spring.io/browse/SPR-16924

7

@rstoyanchev 当然,我的意思是响应主体。非常感谢您创建问题。

7

SPR-16924 已得到解决。

3

这并不像我想象的那么简单。就目前情况而言,将DispatcherServletbean 添加到会导致、和beansMockMvcAutoConfiguration之间的循环。构建器具有寻找(有一个方法)的定制器。更具体地说,创建寻找bean 的程序。DefaultMockMvcBuilderMockMvcDispatcherServletDispatcherServletSpringBootMockMvcBuilderCustomizeraddFiltersSpringBootMockMvcBuilderCustomizerServletContextInitializerBeansServlet

@snicoll 和我对此进行了交谈,并且由于定制程序执行了部分操作WebMvcAutoConfiguration(从上下文中添加过滤器和 servlet,如果需要为它们创建注册 bean),因此真实设置和模拟设置之间存在差异。

6

正如 @mbhave 上面所说,有一个循环,因为ServletContextInitializerBeans寻找ServletRegistrationBean实例。这些 bean 实际上并没有被使用,因为定制者只关心过滤器。我认为我们可以通过允许ServletContextInitializerBeans配置为仅查找特定子类来打破循环,如果未指定子类则ServletContextInitializer默认为。ServletContextIntializer.class所以,像这样:

public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
        Class<? extends ServletContextInitializer>... initializerTypes) {

然后像这样使用:

private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
    for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
        for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(
                beanFactory, initializerType)) {
            addServletContextInitializerBean(initializerBean.getKey(),
                    initializerBean.getValue(), beanFactory);
        }
    }
}