spring resteasy单元测试

mock框架在web项目中进行单元测试非常方便,resteasy作为一个优秀的rest框架,也为我们提供了mock测试工具,但是并没有替我们集成spring,因此我们编写的Resource类无法完成bean的注入,进行单元测试时比较麻烦。我们希望像springmvc那样非常方便地进行单元测试(http://blog.csdn.net/dwade_mia/article/details/77451605),为了解决该问题,笔者扩展了spring test的代码,完成spring test与resteasy mock的集成。

spring test源码解读

SpringJUnit4ClassRunner

SpringJUnit4ClassRunner集成junit测试入口
TestContextManager由SpringJUnit4ClassRunner创建,负责创建TestContext上下文

下面是一个常用的Mock测试类

@WebAppConfiguration(value="src/main/webapp")
@ContextConfiguration( locations={"classpath*:spring-config/applicationContext.xml"} )
@RunWith( SpringJUnit4ClassRunner.class )
public class BaseApiTest extends AbstractJUnit4SpringContextTests {
       // ......
}

其中RunWith指定SpringJUnit4ClassRunner,而SpringJUnit4ClassRunner重写了createTest方法,在junit启动的时候,会调用createTest方法,包括创建Spring容器,对Test测试类进行属性注入等,下图是方法调用的关系图
spring resteasy单元测试_第1张图片

SpringJunit4ClassRunner.java

    //初始化的时候会创建TestContextManager,用于获取测试基类的信息,比如注解等,由TestContextManager创建
    public SpringJUnit4ClassRunner(Class clazz) throws InitializationError {
        super(clazz);
        if (logger.isDebugEnabled()) {
            logger.debug("SpringJUnit4ClassRunner constructor called with [" + clazz + "]");
        }
        ensureSpringRulesAreNotPresent(clazz);
        this.testContextManager = createTestContextManager(clazz);
    }


    protected TestContextManager createTestContextManager(Class clazz) {
        return new TestContextManager(clazz);
    }

    //重写junit实例化对象的方法
    protected Object createTest() throws Exception {
        Object testInstance = super.createTest();
        getTestContextManager().prepareTestInstance(testInstance);
        return testInstance;
    }

TestExecutionListener

以下是spring自带的TestExecutionListener,比如测试类的属性注入,其中DependencyInjectionTestExecutionListener用于对测试类进行注入属性注入,当然我们也可以在测试类上面添加自定义的监听器
spring resteasy单元测试_第2张图片

在创建测试类实例的时候,需要对测试类进行处理,最终调用TextContextManager的prepareTestInstance方法执行监听器的处理操作,默认包括ServletTestExecutionListener, DirtiesContextBeforeModesTestExecutionListener, DependencyInjectionTestExecutionListener, DirtiesContextTestExecutionListener,如果是继承了AbstractTransactionalJUnit4SpringContextTests的时候,还会添加TransactionTestExecutionListener这个Listener

SpringJunit4ClassRunner.java      
protected Object createTest() throws Exception {
    Object testInstance = super.createTest();
    getTestContextManager().prepareTestInstance(testInstance);
    return testInstance;
}
TestContextManager.java

    @Override
    public void prepareTestInstance(TestContext testContext) throws Exception {
        setUpRequestContextIfNecessary(testContext);
    }

    private void setUpRequestContextIfNecessary(TestContext testContext) {
        if (!isActivated(testContext) || alreadyPopulatedRequestContextHolder(testContext)) {
            return;
        }

        //获取Spring容器,如果没有的话,由测试类设置的@ContextConfiguration信息创建容器
        ApplicationContext context = testContext.getApplicationContext();

        if (context instanceof WebApplicationContext) {
            WebApplicationContext wac = (WebApplicationContext) context;
            ServletContext servletContext = wac.getServletContext();

            // other code......

            MockServletContext mockServletContext = (MockServletContext) servletContext;
            MockHttpServletRequest request = new MockHttpServletRequest(mockServletContext);
            request.setAttribute(CREATED_BY_THE_TESTCONTEXT_FRAMEWORK, Boolean.TRUE);
            MockHttpServletResponse response = new MockHttpServletResponse();
            ServletWebRequest servletWebRequest = new ServletWebRequest(request, response);

            RequestContextHolder.setRequestAttributes(servletWebRequest);
            testContext.setAttribute(POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);
            testContext.setAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE, Boolean.TRUE);

            if (wac instanceof ConfigurableApplicationContext) {
                @SuppressWarnings("resource")
                ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) wac;
                ConfigurableListableBeanFactory bf = configurableApplicationContext.getBeanFactory();

                //我们可以在测试类上面注入以下request、response对象
                bf.registerResolvableDependency(MockHttpServletResponse.class, response);
                bf.registerResolvableDependency(ServletWebRequest.class, servletWebRequest);
            }
        }
    }

下图是一个调用栈,ServerletTestExecutionListner会调用getApplicationContext(),如果当前上下文中没有Spring容器的话,会由cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration)方法创建Spring容器,如果我们想自己创建Spring容器的话,需要注入一个自定义的cacheAwareContextLoaderDelegate,并重写loadContext方法。
spring resteasy单元测试_第3张图片
这个CacheAwareContextLoaderDelegate接口默认只有一个实现DefaultCacheAwareContextLoaderDelegate,我们看下调用逻辑,原来默认是由DefaultTestContext调用的,而DefaultTestContext是由TestContextManager创建的,我们找到TestContextManager
spring resteasy单元测试_第4张图片
spring resteasy单元测试_第5张图片
咱们看下这个TestContextManager的注释,如果我们在测试类上面定义了@BootstrapWith,就使用自定义的,否则会使用默认的DefaultTestContextBootstrapper,如果我们使用了@WebAppConfiguration则会使用WebTestContextBootstrapper,跟进BootstrapUtils.resoveTestContextBootstrapper方法便知这个逻辑处理
spring resteasy单元测试_第6张图片

public TestContextManager(Class testClass) {
        this(BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass)));
    }
BootstrapUtils.java
private static Class resolveDefaultTestContextBootstrapper(Class testClass) throws Exception {
        ClassLoader classLoader = BootstrapUtils.class.getClassLoader();
        AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass,
            WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false);
        if (attributes != null) {
            return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
        }
        return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader);
    }

spring test源码扩展,集成resteasy

首先,看一下TestContextBootstrapper的类图,因为是Web容器,所以我们继承WebTestContextBootstrapper,重写getCacheAwareContextLoaderDelegate()方法,返回我们自定义的CacheAwareContextLoaderDelegate实现类
这里写图片描述

public class ResteasyTestContextBootstrapper extends WebTestContextBootstrapper {

    @Override
    protected CacheAwareContextLoaderDelegate getCacheAwareContextLoaderDelegate() {
        return new ResteasyCacheAwareContextLoaderDelegate();
    }

}
/**
* 参考AbstractGenericWebContextLoader.loadContext的方法,使用MockServletContext
* 创建Listener
* @author huangxf
* @date 2017年4月30日
*/
public class ResteasyCacheAwareContextLoaderDelegate extends
        DefaultCacheAwareContextLoaderDelegate {

    private ServletContext servletContext;

    @Override
    protected ApplicationContext loadContextInternal(
            MergedContextConfiguration mergedConfig)
            throws Exception {

        if (!(mergedConfig instanceof WebMergedContextConfiguration)) {
            throw new IllegalArgumentException(String.format(
                "Cannot load WebApplicationContext from non-web merged context configuration %s. "
                        + "Consider annotating your test class with @WebAppConfiguration.", mergedConfig));
        }
        WebMergedContextConfiguration webMergedConfig = (WebMergedContextConfiguration) mergedConfig;

        String resourceBasePath = webMergedConfig.getResourceBasePath();
        ResourceLoader resourceLoader = resourceBasePath.startsWith(ResourceLoader.CLASSPATH_URL_PREFIX) ? new DefaultResourceLoader()
                : new FileSystemResourceLoader();

        this.servletContext = new MockServletContext( resourceBasePath, resourceLoader );

        StringBuilder locations = new StringBuilder();
        for ( String location : webMergedConfig.getLocations() ) {
            locations.append( location ).append( "," );
        }
        locations.deleteCharAt( locations.length() - 1 );
        servletContext.setInitParameter( "contextConfigLocation", locations.toString() );

        //初始化ServletListener
        ServletContextListener bootstrapListener = new SpringResteasyBootstrap();
        ServletContextEvent event = new ServletContextEvent( servletContext );
        bootstrapListener.contextInitialized( event );

        //存放在上下文中
        servletContext.setAttribute( SpringResteasyBootstrap.class.getName(), bootstrapListener );

        return WebApplicationContextUtils.getWebApplicationContext( servletContext );

    }

    @Override
    public void closeContext(
            MergedContextConfiguration mergedContextConfiguration,
            HierarchyMode hierarchyMode) {
        ServletContextListener listener = (ServletContextListener)servletContext.getAttribute( SpringResteasyBootstrap.class.getName() );
        listener.contextDestroyed( new ServletContextEvent( servletContext ) );
        super.closeContext(mergedContextConfiguration, hierarchyMode);
    }

}

于是我们的Test测试类变成这样:

@WebAppConfiguration(value="src/main/webapp")
@ContextConfiguration( locations={"classpath*:spring-config/applicationContext.xml"} )
@RunWith( SpringJUnit4ClassRunner.class )
@BootstrapWith( value=ResteasyTestContextBootstrapper.class )
public class BaseResteasyTest extends AbstractJUnit4SpringContextTests {

    protected Logger logger = LoggerFactory.getLogger( this.getClass() );

    @Resource
    protected WebApplicationContext wac;

    protected HttpServletDispatcher dispatcher;

    @Before
    public void beforeTest() throws ServletException {

        ServletContext servletContext = wac.getServletContext();
        MockServletConfig config = new MockServletConfig( servletContext );
        this.dispatcher = new HttpServletDispatcher();
        dispatcher.init( config );

    }

    protected void logResponse( MockHttpResponse response ) {
        logger.info( "status:{}", response.getStatus() );
        logger.info( "Response content:{}", response.getContentAsString() );
    }

}

我们由@BootstrapWith指定ResteasyCacheAwareContextLoaderDelegate,由它负责创建相关的ServletListener,由于项目里面集成了Resteasy和Spring,因此创建Resteasy和Spring的监听器,该逻辑与web容器的启动逻辑相同

这样便初始化了Spring和Resteasy的ServletListener,还需要初始化Resteasy的HttpServletDispatcher,我们可以在测试类中可以注入WebapplicationContext,这样便可以获取ServletContext,在@BeforeTest方法中直接new出HttpServletDispatcher实例再调用init方法即可。
那么,如何使用呢?在servlet容器中是根据我们设置的url-pattern去寻找对应的Servlet,从而调用service方法即可,我们也可以用调用HttpServletDispatcher提供的getDispatcher().invoke()方法,只不过是传入的参数不同而已。

public class PaymentApiTest extends BaseResteasyTest {

    @Test
    public void testPayOff() throws Exception {

        GatewayPayOffReq req = new GatewayPayOffReq();
        req.setPartnerId( "10000" );

        String json = "{\"data\":" + JsonUtils.toJson( req ) + "}";

        MockHttpRequest httpRequest = MockHttpRequest.post( "/pay/payoff" )
                .contentType( MediaType.APPLICATION_JSON ).accept( MediaType.APPLICATION_JSON )
                .content( json.getBytes( "UTF-8" ) );
        MockHttpResponse httpResponse = new MockHttpResponse();

        //请求
        dispatcher.getDispatcher().invoke( httpRequest, httpResponse );

        //打印响应结果
        logResponse( httpResponse );

    }

Springmvc MockMvc

另外,我们再来研究下Spring的MockMvc测试类,看下Spring是怎么做的,最终发现逻辑上是一致的,只不过提供了很多功能。

/**
* 使用mock测试web服务
* @author huangxf
* @date 2017年4月12日
*/
@WebAppConfiguration(value="src/main/webapp")
@ContextConfiguration( locations={"classpath*:spring-config/core/application-consumer.xml", 
        "classpath*:spring-config/core/springmvc-servlet.xml"} )
@RunWith( SpringJUnit4ClassRunner.class )
public class BaseControllerTest extends AbstractJUnit4SpringContextTests {

    @Resource
    protected WebApplicationContext wac;

    protected MockMvc mockMvc;

    @Before
    public void beforeTest() {
        mockMvc = MockMvcBuilders.webAppContextSetup( wac ).build();
    }

}

而在这个里面又调用了父类的createMockMvc方法,在这个里面对TestDispatcherServlet(DispatcherServlet的子类)进行初始化,最终返回持有TestDispatcherServlet、Filter实例的MockMvc对象

MockMvcBuilderSupport.java
    protected final MockMvc createMockMvc(Filter[] filters, MockServletConfig servletConfig,
            WebApplicationContext webAppContext, RequestBuilder defaultRequestBuilder,
            List globalResultMatchers, List globalResultHandlers,
            List dispatcherServletCustomizers) {

        ServletContext servletContext = webAppContext.getServletContext();

        TestDispatcherServlet dispatcherServlet = new TestDispatcherServlet(webAppContext);
        if (dispatcherServletCustomizers != null) {
            for (DispatcherServletCustomizer customizers : dispatcherServletCustomizers) {
                customizers.customize(dispatcherServlet);
            }
        }
        try {
            dispatcherServlet.init(servletConfig);
        }
        catch (ServletException ex) {
            // should never happen..
            throw new MockMvcBuildException("Failed to initialize TestDispatcherServlet", ex);
        }

        //创建对象,并持有TestDispatcherServlet和Filter实例
        mockMvc.setDefaultRequest(defaultRequestBuilder);
        mockMvc.setGlobalResultMatchers(globalResultMatchers);
        mockMvc.setGlobalResultHandlers(globalResultHandlers);

        return mockMvc;
    }

在我们调用MockMvc的perform方法发起请求时,在这个方法内部会创建FilterChain的Mock实例MockFilterChain,然后挨个调用Filter的doFilter方法,和真实的Servlet容器一样。在MockFilterChain的构造方法里面,会把dispatcherServlet包装成一个Filter,调用最后一个Filter的doFilter方法时,会调用dispatcherServlet的service()方法,这样和web的流程就相同了。

public ResultActions perform(RequestBuilder requestBuilder) throws Exception {

        MockHttpServletRequest request = requestBuilder.buildRequest(this.servletContext);
        MockHttpServletResponse response = new MockHttpServletResponse();

        if (requestBuilder instanceof SmartRequestBuilder) {
            request = ((SmartRequestBuilder) requestBuilder).postProcessRequest(request);
        }

        final MvcResult mvcResult = new DefaultMvcResult(request, response);
        request.setAttribute(MVC_RESULT_ATTRIBUTE, mvcResult);

        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));

        //在MockFilterChain的构造方法里面,会把dispatcherServlet包装成一个Filter,调用最后一个Filter的doFilter方法时,会调用dispatcherServlet的service()方法
        MockFilterChain filterChain = new MockFilterChain(this.servlet, this.filters);
        filterChain.doFilter(request, response);

        //......

        return new ResultActions() {
            //......            
        };
    }

相关的代码在net.dwade.plugins.resteasy.mock这个包下面,github地址:https://github.com/huangxfchn/dwade/tree/master/framework-plugins

你可能感兴趣的:(源码)