web-dev-qa-db-ja.com

mockMVCにフィルターの初期化ルーチンをテストさせるには何が必要ですか?

次の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初期化ルーチンを実行する必要があることを意味するように思われる、インターネットで利用可能な資料を確認しました。しかし、それは明らかに起こっていません。

私がmockMVC機能を使用して「init」ルーチンをテストする方法を理解するのに本当に行き詰まっているので、提案や推奨は大歓迎です。

22

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();
}
_
35

たくさんのテストの後、これが私たちが採用したものです:

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

}
_
4
Paulo Merson

_Spring Boot_アプリの場合、@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)を使用すると、filter.init()が自動的に呼び出されます。 _@SpringBootTest_がデフォルトのパラメーターで使用されている場合、filter.init()を手動で呼び出す必要があります。

1
pavel

統合テストの代わりに真の単体テストが必要な場合は、org.springframework.mock.web.MockServletConfigも参照してください。

org.springframework:spring-test mavenアーティファクト

モックオブジェクトに設定パラメータを設定できます。 HttpServletRequest、HttpServletResponse、FilterChainのモックもあります。

1
GameSalutes