• JUnit4的ClassRunner
  • MockMvc直接对接口发起请求
  • 桥接ibatis的bean
  • Web到App的路由
  • 后记

在公司维护的项目使用的框架很老(内部自研,基于Spring2实现的),单元测试框架使用的JUnit3。日常工作开发调试和自测两种办法:启动服务(weblogic,要打包启动,慢)、单元测试(较快,调试方便)。但老的写单测实在是很繁琐:先继承一个单元测试基类,覆盖其中获取配置文件方法(相当于配置context文件),再在另外两个配置文件中修改(与业务耦合的很紧),然后开始从context中getBean,然后你的准备工作终于做好了可以开始测试了。尤其对于新同事,有人指导还行,没有的话简直抓瞎(当然如果深入了解一下,也是能轻易搞定的,比如我哈哈哈)。思来想去决定:controller的单测,可以简化步骤(比如获取controller bean然后再调用对应方法这一步);加入自动依赖注入,就像使用@Autowired一样(当前项目中还是使用的全XML配置方式);将配置集中起来一个地方管理(使用注解);升级到JUnit4.12。

JUnit4的ClassRunner

基于JUnit4的扩展,主要是利用其提供的ClassRunner,JUnit4.12默认的是BlockJUnit4ClassRunner,于是我们扩展该类,看看能在这里做点什么。

首先来看必须覆盖的构造器,构造参数clazz就是当前测试类的class。除了调用父类构造器,在此处还加了一步Pafa3TestContext.initContext,初始化Ioc容器,以及保存一些测试时需要的上下文信息。

然后注意createTest这个方法,事实上JUnit会根据测试class生成对应的实例。之前说过还实现了自动DI,那么很显然这一步在生成instance之后做再合适不过了,具体就是prepareAutoInject方法,至此自动DI已经实现,在测试类里@AutoInject private SomeController controller就可以直接获取到bean了,当然也提供了可以根据id获取bean。

public class Pafa3Junit4ClassRunner extends BlockJUnit4ClassRunner {public Pafa3Junit4ClassRunner(Class<?> clazz) throws Exception {super(clazz);Pafa3TestContext.initContext(getTestClass().getJavaClass());}@Overrideprotected Object createTest() throws Exception {Object instance = super.createTest();prepareAutoInject(instance);return instance;}private void prepareAutoInject(Object instance) throws IllegalAccessException {TestClass testClass = getTestClass();List<FrameworkField> frameworkFields = testClass.getAnnotatedFields(AutoInject.class);for (FrameworkField frameworkField : frameworkFields) {Object bean;String beanName = frameworkField.getAnnotation(AutoInject.class).value();if (!"".equals(beanName)) {bean = Pafa3TestContext.getContext().getBean(beanName);} else {Class<?> beanType = frameworkField.getType();Map beansOfType = Pafa3TestContext.getContext().getBeansOfType(beanType, true, true);Iterator it = beansOfType.values().iterator();if (it.hasNext()) {bean = it.next();} else {throw new NoSuchBeanDefinitionException(beanType, "no bean type found");}}Field field = frameworkField.getField();field.setAccessible(true);field.set(instance, bean);}}
}
public class Pafa3TestContext {private static ApplicationContext context;private static String[] contextLocations;private static String[] sqlConfigLocations;private static Class<?> clazz;private Pafa3TestContext() {}public static void initContext(Class<?> clazz) {Pafa3TestContext.clazz = clazz;initConfigLocations();initContext();}public static ApplicationContext getContext() {return context;}public static String[] getContextLocations() {return contextLocations;}public static String[] getSqlConfigLocations() {return sqlConfigLocations;}private static void initConfigLocations() {ContextLocations annotation = clazz.getAnnotation(ContextLocations.class);if (annotation == null) {throw new IllegalStateException("test class should be annotated with ContextLocations");}sqlConfigLocations = annotation.sqlMap();String[] locations = annotation.context();int len = locations.length;// 业务定制的,为了少写俩,直接先写死吧contextLocations = Arrays.copyOf(locations, len + 2); contextLocations[len] = "classpath:biz-context.xml";contextLocations[len + 1] = "classpath:common-context.xml";}private static void initContext() {if (context == null) {synchronized (Pafa3TestContext.class) {if (context == null) {context = new ClassPathXmlApplicationContext(getContextLocations());}}}}
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface ContextLocations {String[] context();String[] sqlMap() default {};}@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Inherited
public @interface AutoInject {String value() default "";
}

MockMvc直接对接口发起请求

原来对controller的测试是要先获取这个controller的bean,然后调用接口实际对应的方法。这里其实复杂了,因为bean都是同一个类型的,获取哪一个并没有区别。如果有给定接口,实际已经得到了实际要调用的方法,这个对应关系,也是定义在一个MethodNameResolver类型的bean里的,显然可以从我们的Pafa3TestContext里获取到(因为这时候已经初始化好了)。

public class MockMvcResult {private ModelAndView modelAndView;private String content;public MockMvcResult(ModelAndView modelAndView, String content) {this.modelAndView = modelAndView;this.content = content;}public Object getModel() {return modelAndView == null ? null : modelAndView.getModel();}public Object getView() {return modelAndView == null ? null : modelAndView.getView();}public String getContentAsString() {return content;}
}public interface MockMvc {MockMvcResult request() throws Exception;
}public class StandaloneMockMvc implements MockMvc {private final ApplicationContext context = Pafa3TestContext.getContext();private final String url;private final MockHttpServletRequest request;private final MockHttpServletResponse response;public StandaloneMockMvc(StandaloneMockMvcBuilder builder) {this.url = builder.getUrl();this.request = builder.getRequest();this.response = builder.getResponse();}@Overridepublic MockMvcResult request() throws Exception {Map beanMap = context.getBeansOfType(MethodNameResolver.class, true, true);if (beanMap == null || beanMap.isEmpty()) {throw new NoSuchBeanDefinitionException(MethodNameResolver.class, "ensure add the web context file");}String methodName = null;Iterator it = beanMap.values().iterator();while (it.hasNext() && methodName == null) {MethodNameResolver resolver = (MethodNameResolver) it.next();try {methodName = resolver.getHandlerMethodName(request);} catch (NoSuchRequestHandlingMethodException ignored) {}}if (methodName == null) {throw new NoSuchRequestHandlingMethodException(request);}Object controller = context.getBean(url);return dispatchRequest(methodName, controller);}private MockMvcResult dispatchRequest(String methodName, Object controller) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {Method handleMethod = controller.getClass().getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);Object result = handleMethod.invoke(controller, request, response);if (result == null) {return new MockMvcResult(null, response.getContentAsString());}if (ModelAndView.class.isAssignableFrom(result.getClass())) {return new MockMvcResult((ModelAndView) result, null);}return null;}
}public class StandaloneMockMvcBuilder {private static final String SESSION_USER = "userinformation";private final String url;private final String method;private final MockHttpServletRequest request;private final MockHttpServletResponse response;public StandaloneMockMvcBuilder(String url) {this("GET", url);}public StandaloneMockMvcBuilder(String method, String url) {this.url = url;this.method = method;this.request = new MockHttpServletRequest(null, this.method, this.url);this.response = new MockHttpServletResponse();}public StandaloneMockMvcBuilder addParameter(String name, String value) {request.addParameter(name, value);return this;}public String getUrl() {return url;}public String getMethod() {return method;}public MockHttpServletRequest getRequest() {return request;}public MockHttpServletResponse getResponse() {return response;}public StandaloneMockMvcBuilder withUser(String uid) {UserInformationVO user = new UserInformationVO();user.setUID(uid);return withUser(user);}public StandaloneMockMvcBuilder withUser(UserInformationVO user) {request.getSession().setAttribute(SESSION_USER, user);return this;}public StandaloneMockMvc build() {return new StandaloneMockMvc(this);}
}

至此,我们可以直接构造对应的URL以及相关参数,使用MockMvc发起请求等待结果了。

桥接ibatis的bean

以上两点完成后,还差一个连接数据库的bean。项目中使用的是ibatis,读取的sqlmap是定义在一个sqlmap-config.xml里,该配置包含所有的sqlmap(按功能模块分的),然后由SqlMapClientFactoryBean来读取sqlmap-config.xml。由于配置都集中管理在ContextLocations注解里了,所以这里也需要重新实现,用了一个小聪明,直接根据配置的sqlMapConfig生成一个XML内容交给SqlMapClientFactoryBean去读取。

public class SimpleSqlMapClientFactoryBean extends SqlMapClientFactoryBean {@Overridepublic void afterPropertiesSet() throws IOException {Resource configLocation = getSqlConfigResource();super.setConfigLocation(configLocation);super.afterPropertiesSet();}private Resource getSqlConfigResource() {String[] configLocations = Pafa3TestContext.getSqlConfigLocations();if (configLocations == null || configLocations.length == 0) {return new ClassPathResource("sqlmap-config.xml");}return builtXMLResource(configLocations);}private Resource builtXMLResource(String[] configLocations) {final String xmlAsString = buildSqlMapConfigContent(configLocations);return new AbstractResource() {@Overridepublic InputStream getInputStream() throws IOException {return new ByteArrayInputStream(xmlAsString.getBytes("UTF-8"));}@Overridepublic String getDescription() {return "XML built as string: " + xmlAsString;}};}private String buildSqlMapConfigContent(String[] configLocations) {Document document = DocumentHelper.createDocument();document.setXMLEncoding("UTF-8");document.addDocType("sqlMapConfig", "-//iBATIS.com//DTD SQL Map Config 2.0//EN", "http://www.ibatis.com/dtd/sql-map-config-2.dtd");Element sqlMapConfig = document.addElement("sqlMapConfig");Element setting = sqlMapConfig.addElement("settings");setting.addAttribute("cacheModelsEnabled", "true");setting.addAttribute("enhancementEnabled", "false");setting.addAttribute("lazyLoadingEnabled", "false");setting.addAttribute("maxRequests", "3000");setting.addAttribute("maxSessions", "3000");setting.addAttribute("maxTransactions", "3000");setting.addAttribute("useStatementNamespaces", "true");for (String location : configLocations) {Element sqlMap = sqlMapConfig.addElement("sqlMap");sqlMap.addAttribute("resource", location);}return document.asXML();}
}

Web到App的路由

项目是分层部署的,分为了Web(DMZ区)和App(内网)两层,前者就是controller所在,然后远程调用App层的Action(通过EJB)。在本地单元测试,显然不会去构造一个EJB容器环境,而是直接通过本地同一个JVM调用即可(项目中调用的bean的名字是写死的),于是实现一个本地的ApplicationController

public class AppControllerFactoryBean implements FactoryBean {private ApplicationController proxy;@Overridepublic Object getObject() throws Exception {if (proxy == null) {proxy = getProxy();}return proxy;}@Overridepublic Class getObjectType() {return proxy != null ? proxy.getClass() : ApplicationController.class;}@Overridepublic boolean isSingleton() {return true;}private ApplicationController getProxy() {return (ApplicationController) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),new Class[]{ApplicationController.class}, new LocalProxyAppControllerInvocationHandler());}
}public class LocalProxyAppControllerInvocationHandler implements InvocationHandler {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName();Class<?>[] parameterTypes = method.getParameterTypes();if (method.getDeclaringClass() == Object.class) {throw new UnsupportedOperationException("unsupported method: " + method);}if ("toString".equals(methodName) && parameterTypes.length == 0) {return "proxy of ApplicationController";}if ("hashCode".equals(methodName) && parameterTypes.length == 0) {return 1;}if ("equals".equals(methodName) && parameterTypes.length == 1) {return Boolean.FALSE;}if (args.length != 1 || !(args[0] instanceof ServiceRequest)) {throw new IllegalArgumentException("arguments length not 1 or not type of ServiceRequest");}return invokeLocal((ServiceRequest) args[0]);}private Object invokeLocal(ServiceRequest request) throws BusinessServiceException {String beanName = request.getRequestedServiceID();Action action = (Action) Pafa3TestContext.getContext().getBean(beanName);return action.perform(request);}
}

写完之后发现,似乎不用动态代理,直接实现ApplicationController就行了= =||。不过鉴于都写出来了,暂时先用着吧。主要是提醒看代码的同志,toString, equals, hashCode三个方法,在动态代理时也是会被代理的。

后记

大功告成,现在写单元测试的效率比之前提高的简直不要太多。终于不用东配置一下西添加一下了(而且有两个还是重复的),对团队的提升自我感觉还是比较多的。但是有啥借鉴的么?我觉得没啥,都是被老项目老框架逼出来的轮子,毕竟新框架直接上Spring的test即可,功能强大好用。顺便吐槽一下公司:老项目难升级情有可原,但是2017年新启动的项目,还有必要继续jdk1.6 + weblogic + spring3.1吗?

转载于:https://www.cnblogs.com/dirac/p/8846905.html

基于JUnit4扩展老项目的UT框架且自动DI相关推荐

  1. 老项目的#iPhone6与iPhone6Plus适配#iOS8无法开启定位问题和#解决方案#

    本文永久地址为 http://www.cnblogs.com/ChenYilong/p/4020359.html,转载请注明出处. iOS8的定位和推送的访问都发生了变化, 下面是iOS7和iOS8申 ...

  2. 能被选为2021最佳开源项目的WEB框架究竟有多棒?

    喜欢开源的小伙伴,想必或多或少听说过InfoWorld的年度最佳开源软件评选 今年呢,哦不,应该说去年,也是评选出来了很多优秀的开源项目,覆盖了软件开发.云计算.机器学习等多个不同的领域. TJ君呢今 ...

  3. Django框架(4.django中进入项目的shell之后对数据表进行增删改查的操作)

    django设计模型类.模型类生成表.ORM框架简介:https://blog.csdn.net/wei18791957243/article/details/88657270 数据操作 完成数据表的 ...

  4. 基于SpringBoot项目的https

    基于SpringBoot项目的https 在spring中配置项目运行的端口很简单. 在application.properties中 server.port: 8080 这样配置后,spring b ...

  5. 【Struts2】Struts2框架创建web项目的6个步骤

    Struts2框架创建web项目的6个步骤 1.创建WEB项目 2.导入Struts2核心jar包 3.在web.xml文件中配置前端控制器filter ※如果Struts2框架是2.1.3之后的版本 ...

  6. 关于白石画廊香港分社发起基于区块链技术的美术品在线拍卖平台开发项目的ICO众筹的公告

    香港 -- (美国商业资讯) -- 白石画廊香港位于香港(地址:香港特区黄竹坑道12号香华工业大厦6楼,董事会理事长:白石幸生https://www.whitestone-gallery.com/)宣 ...

  7. Entity Framework 实体框架的形成之旅--基于泛型的仓储模式的实体框架(1)

    很久没有写博客了,一些读者也经常问问一些问题,不过最近我确实也很忙,除了处理日常工作外,平常主要的时间也花在了继续研究微软的实体框架(EntityFramework)方面了.这个实体框架加入了很多特性 ...

  8. 开箱即用~基于.NET Core的统一应用逻辑分层框架设计

    目前公司系统多个应用分层结构各不相同,给运维和未来的开发带来了巨大的成本,分层架构看似很简单,但保证整个研发中心都使用统一的分层架构就不容易了. 那么如何保证整个研发中心都使用统一的分层架构,以达到提 ...

  9. 基于DDD的abp模式的新框架

    ABP框架背景知识介绍 ABP是ASP.NET Boilerplate的简称,ABP是一个开源且文档友好的应用程序框架. ABP不仅仅是一个框架,它还提供了一个最徍实践的基于领域驱动设计(DDD)的体 ...

最新文章

  1. VTL-vm模板的变量用法
  2. HorizontalTable
  3. 20160406作业
  4. mysql memcached 使用场景_memcache的应用场景?
  5. Linux 常用命令笔记
  6. 【FFmpeg】ffmpeg 命令查询二 ( 比特流过滤器 | 可用协议 | 过滤器 | 像素格式 | 标准声道布局 | 音频采样格式 | 颜色名称 )
  7. 红帽子怎么vi编译c语言,在RedHat5.3上编译和配置Vim
  8. python数据收集系统_玩玩Python数据采集_001
  9. Hexo+腾讯CVM+又拍云+github+gitee+coding
  10. Boost:异步操作,需要boost :: asio :: async_compose函数的测试程序
  11. 金士顿固态硬盘计算机如何识别,金士顿SV300 SF2281固态硬盘SSD不认盘开卡修复教程...
  12. 【转载】用廉价的315M遥控模块实现数据传输
  13. 第十六节:ES6新增的 Set 和 WeakSet 是什么东西?
  14. JavaScript学习(九)—练习:实现跳转页面
  15. stm32+esp8266+app inventor简单小制作
  16. C语言rs485编程,- 第六讲 单片机之c语言RS485通信
  17. 相机存储卡不小心格式化怎么恢复呢?
  18. python学习004-----python中%s的各种用法
  19. 利用scp 在linux之间传输文件
  20. 扫地机器人自动回充原理

热门文章

  1. Unity绿背景抠图插件
  2. 该如何旋转EDIUS 8中的图片
  3. Mars说光场(2)— 光场与人眼立体成像机理
  4. 3D视角旋转平移鼠标响应制作
  5. nodejs 解析Android apk获取app icon
  6. 基于YOLOv5 + Deepsort 的多车辆追踪与测速
  7. WPF 四种尺寸单位
  8. npm 初始化_初始化npm的最佳时间
  9. 企业如何实现高效的协作办公?
  10. 关于转行软件测试行业必备课程学习的解答