如何使mockMVC测试过滤器的初始化程序?

28

我已经实现了以下CORS过滤器,当代码在服务器上执行时可以正常工作:

/*
 *    Copyright 2013 BrandsEye (http://www.brandseye.com)
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.energyos.espi.datacustodian.web.filter;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.stereotype.Component;

/**
 * Adds CORS headers to requests to enable cross-domain access.
 */

@Component
public class CORSFilter implements Filter {

    private final Log logger = LogFactory.getLog(getClass());
    private final Map<String, String> optionsHeaders = new LinkedHashMap<String, String>();

    private Pattern allowOriginRegex;
    private String allowOrigin;
    private String exposeHeaders;

    public void init(FilterConfig cfg) throws ServletException {
        String regex = cfg.getInitParameter("allow.origin.regex");
        if (regex != null) {
            allowOriginRegex = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
        } else {
            optionsHeaders.put("Access-Control-Allow-Origin", "*");
        }

        optionsHeaders.put("Access-Control-Allow-Headers", "Origin, Authorization, Accept, Content-Type");
        optionsHeaders.put("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        optionsHeaders.put("Access-Control-Max-Age", "1800");
        for (Enumeration<String> i = cfg.getInitParameterNames(); i.hasMoreElements(); ) {
            String name = i.nextElement();
            if (name.startsWith("header:")) {
                optionsHeaders.put(name.substring(7), cfg.getInitParameter(name));
            }
        }

        //maintained for backward compatibility on how to set allowOrigin if not
        //using a regex
        allowOrigin = optionsHeaders.get("Access-Control-Allow-Origin");
        //since all methods now go through checkOrigin() to apply the Access-Control-Allow-Origin
        //header, and that header should have a single value of the requesting Origin since
        //Access-Control-Allow-Credentials is always true, we remove it from the options headers
        optionsHeaders.remove("Access-Control-Allow-Origin");

        exposeHeaders = cfg.getInitParameter("expose.headers");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {          
            logger.debug("CORSFilter processing: Checking for Cross Origin pre-flight OPTIONS message");
        }

        if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
            HttpServletRequest req = (HttpServletRequest)request;
            HttpServletResponse resp = (HttpServletResponse)response;
            if ("OPTIONS".equals(req.getMethod())) {
                allowOrigin = "*";                                          //%%%%% Test force of allowOrigin
                if (checkOrigin(req, resp)) {
                    for (Map.Entry<String, String> e : optionsHeaders.entrySet()) {
                        resp.addHeader(e.getKey(), e.getValue());
                    }

                    // We need to return here since we don't want the chain to further process
                    // a preflight request since this can lead to unexpected processing of the preflighted
                    // request or a 40x - Response Code
                    return;

                }
            } else if (checkOrigin(req, resp)) {
                if (exposeHeaders != null) {
                    resp.addHeader("Access-Control-Expose-Headers", exposeHeaders);
                }
            }
        }
        filterChain.doFilter(request, response);
    }

    private boolean checkOrigin(HttpServletRequest req, HttpServletResponse resp) {
        String origin = req.getHeader("Origin");
        if (origin == null) {
            //no origin; per W3C specification, terminate further processing for both pre-flight and actual requests
            return false;
        }

        boolean matches = false;
        //check if using regex to match origin
        if (allowOriginRegex != null) {
            matches = allowOriginRegex.matcher(origin).matches();
        } else if (allowOrigin != null) {
            matches = allowOrigin.equals("*") || allowOrigin.equals(origin);
        }

        if (matches) {

            // Activate next two lines and comment out third line if Credential Support is required
//          resp.addHeader("Access-Control-Allow-Origin", origin);
//          resp.addHeader("Access-Control-Allow-Credentials", "true");         
            resp.addHeader("Access-Control-Allow-Origin", "*");
            return true;
        } else {
            return false;
        }
    }

    public void destroy() {
    }
}

以下JUnit测试使用mockMVC,但失败了,因为CORSFilter的“init”逻辑没有执行(通过在JUnit测试中打断点证明):

package org.energyos.espi.datacustodian.integration.web.filters;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.FilterConfig;

import org.energyos.espi.datacustodian.web.filter.CORSFilter;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.context.WebApplicationContext;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("/spring/test-context.xml")
@Profile("test")
public class CORSFilterTests {

    private final Log logger = LogFactory.getLog(getClass());   

    @Autowired
    private CORSFilter filter;

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = webAppContextSetup(this.wac)
                .addFilters(filter).build();
    }

    @Test
    public void optionsResponse_hasCorrectFilters() throws Exception {

        RequestBuilder requestBuilder = MockMvcRequestBuilders.options("/DataCustodian/oauth/token")
                .header("Origin", "foobar")
                .header("Access-Control-Allow-Origin", "*"); 

        MvcResult result =   mockMvc.perform(requestBuilder)
                .andExpect(header().string("Access-Control-Allow-Origin", is("*")))
                .andExpect(header().string("Access-Control-Allow-Methods", is("GET, POST, PUT, DELETE, OPTIONS")))
                .andExpect(header().string("Access-Control-Allow-Headers", is("origin, authorization, accept, content-type")))
                .andExpect(header().string("Access-Control-Max-Age", is("1800")))               
                .andReturn();      
        }
    }
}

我已经查阅了互联网上的相关资料,似乎暗示着mockMVC @Before部分中的“.addfilter(filter)”元素应该执行CORSFilter init例程。然而,这显然并没有发生。
如果您对如何使用mockMVC能力测试“init”程序感到困惑,那么任何建议或推荐将不胜感激。

你好!您有没有解决这个问题的方案:https://stackoverflow.com/questions/53774909/how-to-bypass-or-skip-customfilter-in-mockito-with-springboot-applicaiton 。非常感谢您的帮助。 - MrSham
4个回答

41

Spring MVC测试套件不是用来测试容器配置的,它的作用是测试您的MVC(@Controller和其他映射)配置。 Filter#init(ServletConfig) 是由容器管理的方法。

如果您确实需要测试容器配置,也可以进行模拟测试。

@Before
public void setup() {
    filter.init(someMockFilterConfig); // using a mock that you construct with init params and all
    this.mockMvc = webAppContextSetup(this.wac)
            .addFilters(filter).build();
}

我无法实现您建议的修复,但想接受您的答案,但我不确定如何做到这一点(抱歉,这是我第一次发帖)。 我让CORSFilter检查“JUnit_Test”的来源,然后提供CORSFilter初始程序在服务器加载过滤器时提供的数据。 我想了解您提出的解决方案,但由于它本质上与我添加的代码相同,我采取了捷径以节省时间。 - Donald F. Coffin
@donald 我从你的问题中做出了太多的假设。你可以使用像Mockito这样的库来_mock_对象。这些模拟对象表现出你想要的行为。例如,一个Filter需要整个servlet容器,但不需要使用mocks。查看链接中的示例以及网络上的其他示例。如果您想接受我的答案,可以在我的答案分数旁边勾选复选框。 - Sotirios Delimanolis

8

经过大量测试,我们采用了以下方案:

  • 对于测试 @RestController ,请使用 MockMvc。
  • 对于测试过滤器或其他基础设施元素,请使用 TestRestTemplate

使用 MockMvc,addFilter(Filter) 并不会执行过滤器。而使用 TestRestTemplate 的解决方案更为原始,但是您应用程序/库中配置的所有过滤器都会被执行。例如:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = MySpringBootApplication.class, webEnvironment= 
SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MyRestControllerTest {

    @LocalServerPort
    private int port;

    @Test
    public void myTestCase() throws Exception {

        HttpStatus expectedStatusCode = HttpStatus.OK;
        String expectedResponseBody = "{\"someProperty\" : \"someValue\" }";

        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Bearer YourTokenJwtForExample");

        HttpEntity<String> entity = new HttpEntity<>(null, headers);

        TestRestTemplate restTemplate = new TestRestTemplate();
        ResponseEntity<String> response = restTemplate.exchange(
            "http://localhost:" + port + "/my-rest-uri",
            HttpMethod.GET, entity, String.class);

        Assert.assertEquals(expectedStatusCode, response.getStatusCode());
        Assert.assertEquals(expectedResponseBody, response.getBody());
    }

}

2
如果您在TestRestTemplate中使用自动装配,则可以省略主机和端口。 - thomas77
@Paulo Merson我在Filters方面有类似的问题。 你可以请检查链接https://stackoverflow.com/questions/74203660/unable-to-inject-custom-filter-while-performing-integration-testing-using-spring - Sachin HR

3

对于一个Spring Boot应用程序,如果使用@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT),则会自动调用filter.init()方法。如果使用默认参数的@SpringBootTest,则需要手动调用filter.init()方法。


这是最简单的解决方案。 - Arun A

2
如果您想要一个真正的单元测试而不是集成测试,您可能还想看一下org.springframework.mock.web.MockServletConfig,它可以从org.springframework:spring-test maven中获取。
您可以在模拟对象上设置配置参数。此外,还有HttpServletRequest、HttpServletResponse和FilterChain的模拟。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接