spring test集成resteasy mock单元测试
来源:互联网 发布:点读包音频切割软件 编辑:程序博客网 时间:2024/05/16 09:12
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测试类进行属性注入等,下图是方法调用的关系图
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用于对测试类进行注入属性注入,当然我们也可以在测试类上面添加自定义的监听器
在创建测试类实例的时候,需要对测试类进行处理,最终调用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方法。
这个CacheAwareContextLoaderDelegate接口默认只有一个实现DefaultCacheAwareContextLoaderDelegate,我们看下调用逻辑,原来默认是由DefaultTestContext调用的,而DefaultTestContext是由TestContextManager创建的,我们找到TestContextManager
咱们看下这个TestContextManager的注释,如果我们在测试类上面定义了@BootstrapWith,就使用自定义的,否则会使用默认的DefaultTestContextBootstrapper,如果我们使用了@WebAppConfiguration则会使用WebTestContextBootstrapper,跟进BootstrapUtils.resoveTestContextBootstrapper方法便知这个逻辑处理
public TestContextManager(Class<?> testClass) { this(BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass))); }
BootstrapUtils.javaprivate 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<ResultMatcher> globalResultMatchers, List<ResultHandler> globalResultHandlers, List<DispatcherServletCustomizer> 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
- spring test集成resteasy mock单元测试
- resteasy 与spring集成
- spring集成resteasy实例
- Spring MVC的单元测试和集成测试(不使用mock)
- Spring MVC的单元测试和集成测试(不使用mock)
- spring-mock单元测试新方法
- Spring Mock单元测试
- spring Mock Test
- spring-test 加 mock
- C单元测试之Mock Test篇
- spring mvc test by mock
- Spring Mock--用于Spring 的单元测试
- Spring-Mock--用于Spring 的单元测试
- Spring和resteasy集成三种方式
- Spring和resteasy集成三种方式
- spring-mvc-test(单元测试)
- 集成单元测试google test (转载)
- 使用spring-mock进行dao单元测试
- 剑指Offer [07] 斐波那契数列
- 短时平均能量
- 《程序员的修炼之道》笔记——5、你的知识资产
- I/O流学习--InputStream下的常用方法
- Your pip version is out of date, please install pip >= 9.0.1.
- spring test集成resteasy mock单元测试
- 成员变量的隐藏
- setfacl使用
- 用Intellij Idea创建maven项目 web项目
- 用eclipse开发项目时遇到的常见错误整理,和配套解决方案
- html入门练手必备之表格及其代码
- poj 1065
- Ubuntu 配置网络
- 使用脚本,免密码登陆