日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發(fā)布時(shí)間:2023/12/29 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于JUnit4扩展老项目的UT框架且自动DI 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
  • JUnit4的ClassRunner
  • MockMvc直接對(duì)接口發(fā)起請(qǐng)求
  • 橋接ibatis的bean
  • Web到App的路由
  • 后記

在公司維護(hù)的項(xiàng)目使用的框架很老(內(nèi)部自研,基于Spring2實(shí)現(xiàn)的),單元測(cè)試框架使用的JUnit3。日常工作開發(fā)調(diào)試和自測(cè)兩種辦法:啟動(dòng)服務(wù)(weblogic,要打包啟動(dòng),慢)、單元測(cè)試(較快,調(diào)試方便)。但老的寫單測(cè)實(shí)在是很繁瑣:先繼承一個(gè)單元測(cè)試基類,覆蓋其中獲取配置文件方法(相當(dāng)于配置context文件),再在另外兩個(gè)配置文件中修改(與業(yè)務(wù)耦合的很緊),然后開始從context中g(shù)etBean,然后你的準(zhǔn)備工作終于做好了可以開始測(cè)試了。尤其對(duì)于新同事,有人指導(dǎo)還行,沒有的話簡(jiǎn)直抓瞎(當(dāng)然如果深入了解一下,也是能輕易搞定的,比如我哈哈哈)。思來想去決定:controller的單測(cè),可以簡(jiǎn)化步驟(比如獲取controller bean然后再調(diào)用對(duì)應(yīng)方法這一步);加入自動(dòng)依賴注入,就像使用@Autowired一樣(當(dāng)前項(xiàng)目中還是使用的全XML配置方式);將配置集中起來一個(gè)地方管理(使用注解);升級(jí)到JUnit4.12。

JUnit4的ClassRunner

基于JUnit4的擴(kuò)展,主要是利用其提供的ClassRunner,JUnit4.12默認(rèn)的是BlockJUnit4ClassRunner,于是我們擴(kuò)展該類,看看能在這里做點(diǎn)什么。

首先來看必須覆蓋的構(gòu)造器,構(gòu)造參數(shù)clazz就是當(dāng)前測(cè)試類的class。除了調(diào)用父類構(gòu)造器,在此處還加了一步Pafa3TestContext.initContext,初始化Ioc容器,以及保存一些測(cè)試時(shí)需要的上下文信息。

然后注意createTest這個(gè)方法,事實(shí)上JUnit會(huì)根據(jù)測(cè)試class生成對(duì)應(yīng)的實(shí)例。之前說過還實(shí)現(xiàn)了自動(dòng)DI,那么很顯然這一步在生成instance之后做再合適不過了,具體就是prepareAutoInject方法,至此自動(dòng)DI已經(jīng)實(shí)現(xiàn),在測(cè)試類里@AutoInject private SomeController controller就可以直接獲取到bean了,當(dāng)然也提供了可以根據(jù)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;// 業(yè)務(wù)定制的,為了少寫倆,直接先寫死吧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直接對(duì)接口發(fā)起請(qǐng)求

原來對(duì)controller的測(cè)試是要先獲取這個(gè)controller的bean,然后調(diào)用接口實(shí)際對(duì)應(yīng)的方法。這里其實(shí)復(fù)雜了,因?yàn)閎ean都是同一個(gè)類型的,獲取哪一個(gè)并沒有區(qū)別。如果有給定接口,實(shí)際已經(jīng)得到了實(shí)際要調(diào)用的方法,這個(gè)對(duì)應(yīng)關(guān)系,也是定義在一個(gè)MethodNameResolver類型的bean里的,顯然可以從我們的Pafa3TestContext里獲取到(因?yàn)檫@時(shí)候已經(jīng)初始化好了)。

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);} }

至此,我們可以直接構(gòu)造對(duì)應(yīng)的URL以及相關(guān)參數(shù),使用MockMvc發(fā)起請(qǐng)求等待結(jié)果了。

橋接ibatis的bean

以上兩點(diǎn)完成后,還差一個(gè)連接數(shù)據(jù)庫的bean。項(xiàng)目中使用的是ibatis,讀取的sqlmap是定義在一個(gè)sqlmap-config.xml里,該配置包含所有的sqlmap(按功能模塊分的),然后由SqlMapClientFactoryBean來讀取sqlmap-config.xml。由于配置都集中管理在ContextLocations注解里了,所以這里也需要重新實(shí)現(xiàn),用了一個(gè)小聰明,直接根據(jù)配置的sqlMapConfig生成一個(gè)XML內(nèi)容交給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的路由

項(xiàng)目是分層部署的,分為了Web(DMZ區(qū))和App(內(nèi)網(wǎng))兩層,前者就是controller所在,然后遠(yuǎn)程調(diào)用App層的Action(通過EJB)。在本地單元測(cè)試,顯然不會(huì)去構(gòu)造一個(gè)EJB容器環(huán)境,而是直接通過本地同一個(gè)JVM調(diào)用即可(項(xiàng)目中調(diào)用的bean的名字是寫死的),于是實(shí)現(xiàn)一個(gè)本地的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);} }

寫完之后發(fā)現(xiàn),似乎不用動(dòng)態(tài)代理,直接實(shí)現(xiàn)ApplicationController就行了= =||。不過鑒于都寫出來了,暫時(shí)先用著吧。主要是提醒看代碼的同志,toString, equals, hashCode三個(gè)方法,在動(dòng)態(tài)代理時(shí)也是會(huì)被代理的。

后記

大功告成,現(xiàn)在寫單元測(cè)試的效率比之前提高的簡(jiǎn)直不要太多。終于不用東配置一下西添加一下了(而且有兩個(gè)還是重復(fù)的),對(duì)團(tuán)隊(duì)的提升自我感覺還是比較多的。但是有啥借鑒的么?我覺得沒啥,都是被老項(xiàng)目老框架逼出來的輪子,畢竟新框架直接上Spring的test即可,功能強(qiáng)大好用。順便吐槽一下公司:老項(xiàng)目難升級(jí)情有可原,但是2017年新啟動(dòng)的項(xiàng)目,還有必要繼續(xù)jdk1.6 + weblogic + spring3.1嗎?

轉(zhuǎn)載于:https://www.cnblogs.com/dirac/p/8846905.html

總結(jié)

以上是生活随笔為你收集整理的基于JUnit4扩展老项目的UT框架且自动DI的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。