`

我摸、我摸、我摸摸摸——提高代码可测试性

阅读更多

 

      虽然有了EasyMock这样的摸客工具,但并不一定就表示你的代码好测,在mock对象创建完成后,你的代码得有能力让这些mock对象注入到你的对象中去,这样EasyMock才能有用武之地,也就是说,只有当代码基于IOC原则实现的,才能使EasyMock发挥真正的作用。

      满足以下条件的代码都是无法通过创建mock对象来测试的:

1.在代码内自己查找依赖。如在代码中直接new某个接口的实现类;

2.代码依赖于单例类的实例。由于单例类的构造方法是private的,所以无法创建mock对象。如果使用spring,那么单例的效果可以通过spring的配置来实现,这时就可以将类实现为普通类,而EasyMock就可以起作用了;

3.代码依赖与某些类的静态方法的执行结果。静态方法是无法mock的,所以不要在静态方法中实现业务逻辑,本身这样实现也是错误的,业务逻辑应该放到相应的领域对象中的功能,静态方法只应该用于实现某些简单逻辑的工具方法;

      在现实中,很多时候我们需要依赖老系统中的接口进行开发,某些接口被实现为单例类或者接口的实例只能通过某个静态方法获取,假如在代码中直接调用这些静态方法来获取实例,那么测试时我们就得强依赖与静态方法的实现,无法对依赖进行mock,如下面的代码:

 

public class AccountService {
	public void doSomething() {
		AccountDao.getInstance().insert();
	}
}

public class AccountDao {
	private static final AccountDao INSTANCE = new AccountDao();
	
	private AccountDao() {
	}
	
	public void insert() {
	}
	
	public static AccountDao getInstance() {
		return INSTANCE;
	}
}

 

      AccountDao是一个单例类,而AccountService直接通过AccountDao.getInstance()来获取依赖,这样的代码在UT时是很难测试的,因为无法对AccountDao进行mock。这里将介绍几种解决的方法。

第一种方法,先来看点代码:

public class AccountService {
	private AccountDao accountDao;

	public void setAccountDao(AccountDao accountDao) {
		this.accountDao = accountDao;
	}

	public AccountDao getAccountDao() {
		if (accountDao == null) {
			accountDao = AccountDao.getInstance();
		}
		return accountDao;
	}

	public void doSomething() {
		getAccountDao().insert();
	}
}

      这里为AccountService增加了accountDao属性,并为它添加geAccount、setAccount方法,代码中依赖accountDao的地方都通过getAccount()获取实例,同时在getAccount方法中弄了点小技量,先判断accountDao是否为null,为null就调用AccountDao.getInstance()来获取AccountDao的实例。虽然这里有get、set方法,但实际上并没有真正的使用IOC原则,因为我们还是在AccountService中去查找依赖了。而setAccount方法也只是为了UT而留下的一个小后门,因为这时我们可以在UT时将mock对象注入,但这儿set方法在系统运行过程中根本不会使用,就只是为了让我们的代码好测,感觉是有点别扭。
      第二种方法,假如你的系统中已经在使用Spring,那么可以通过Spring提供的方法替换(Method Replacement)来实现依赖注入。通过方法替换,你可以在不修改任何源代码的前提下,替换任何非final类的非静态方法。
Spring的方法替换是通过CGLIB在运行期动态创建类的子类来实现的,随后对方法的调用就会被重定向到另一个类上,这个类需要实现MethodReplacer接口:
import java.lang.reflect.Method;
import org.springframework.beans.factory.support.MethodReplacer;

public class AccountDaoReplacer implements MethodReplacer {
	public Object reimplement(Object arg0, Method arg1, Object[] arg2)
			throws Throwable {
		return AccountDao.getInstance();
	}
}


public class AccountService {
	private AccountDao accountDao;

	public void setAccountDao(AccountDao accountDao) {
		this.accountDao = accountDao;
	}

	public AccountDao getAccountDao() {
		return accountDao;
	}

	public void doSomething() {
		getAccountDao().insert();
	}
}

      我们实现了MethodReplacer的reimplement方法,方法的第一个参数是原有方法被调用的那个对象,第一个参数表示要覆盖的方法,第三个参数是方法调用时传入的参数,这个方法必须返回重新实现后的逻辑结果。再看看现在的AccountService,惟一的改动就是getAccount()方法的if分支被删除掉了。代码写完后,还需要在spring的配置文件中做如下配置才能达到我们预期的目标:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<bean id="accountServiceReplacer" class="AccountDaoReplacer"></bean>
	<bean id="accountService" class="AccountService">
		<replaced-method name="getAccountDao" replacer="accountServiceReplacer">
</replaced-method>
	</bean>
</beans>

      这里声明了AccountService和AccountDaoReplacer的实例,在AccountService的声明中,我们通过replace-method指明了需要替换的方法,方法名由name属性指定,replacer指向MethodReplacer的bean名称,如果被替换的方法有多个重载方法,那么需要在replace-method中通过arg-type指定参数:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<bean id="accountServiceReplacer" class="AccountDaoReplacer"></bean>
	<bean id="accountService" class="AccountService">
		<replaced-method name="getAccountDao" replacer="accountServiceReplacer">
			<arg-type>String</arg-type>
		</replaced-method>
	</bean>	
</beans>

由于用了CGLIB,性能方面要比第一中方法要稍差些,不过只是这种简单的get方法的替换,这个差异并不明显,调用AccountService的getAccount方法100000次,花了151毫秒,而第一个方法则几乎是0毫秒。另外在这种解决方法中,setAccount方法也纯粹就是为了UT而留下的后门。

      第三种解决方法,是通过Spring的FactoryBean接口来实现。Spring的FactoryBean本来就是用来解决这种无法通过new创建bean的情况的,可以将它当做其它bean的工厂。FactoryBean可以象普通bean一样在Spring中配置,但当Spring使用FactoryBean来查找依赖时,并不返回FactoryBean本身,而是调用Factory.getObject()方法,并以其返回值做为查找的结果。

import org.springframework.beans.factory.FactoryBean;

public class AccountDaoFactoryBean implements FactoryBean {

	public Object getObject() throws Exception {
		return AccountDao.getInstance();
	}

	public Class getObjectType() {
		return AccountDao.class;
	}

	public boolean isSingleton() {
		return true;
	}
}

public class AccountService {
	private AccountDao accountDao;

	public void setAccountDao(AccountDao accountDao) {
		this.accountDao = accountDao;
	}

	public AccountDao getAccountDao() {
		return accountDao;
	}

	public void doSomething() {
		getAccountDao().insert();
	}
}

FactoryBean接口需要实现三个方法:getObject()方法获取FactoryBean要创建的对象,这个才真正是其它bean要依赖的对象;getObjectType()方法返回FactoryBean要创建的对象的class;isSingleton()告诉Spring,FactoryBean创建的对象是否为单例的,这里需要和FactoryBean在Spring中配置<bean>标签时指定的singleton属性区别开来,后者用于告诉Spring该FactoryBean本身是否为单例的,而不是FactoryBean创建的Bean是否为单例。

      代码写完后,在Spring中配置下就可以了,和普通bean的配置没有什么区别:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
	<bean id="accountDaoFactoryBean" class="AccountDaoFactoryBean"></bean>
	<bean id="factoryAccountService" class="AccountService">
		<property name="accountDao" ref="accountDaoFactoryBean"></property>
	</bean>
</beans>

 这里的AccountService实现与第二种方法中的实现完全一样,但在这种方法中,setAccount方法就不是个花瓶了,因为它在系统运行时会被用来注入AccountDao的实例。
      当一个方法中的代码量过多,往往意味着类或方法的设计有问题,职责不明确,在一个类或方法中做了太的事,抛除这个方面,单单从UT的角度来说,这意味着对这个方法的测试工作量会很大,为什么呢?首先方法的代码量多,往往意味着方法中的依赖很多,分支很多,代码的复杂度很高,那么光是为覆盖各种分支而准备的测试数据的代码量就很大,每多写一行代码,往往可能就会多一个问题,象这样子就会经常出现代码测试不通过,然后定位到最后,却是因为测试代码有问题而导致的现象。
      既然一个方法中的代码量不能过多,OK,我重构下,采用将某些相似的逻辑提到一个私有方法中,然后由待测试方法调用抽取出来的私有方法的策略,这样子初初看起来是使待测试方法中的代码量变少了,而且代码可读性提高了不少,但其作用也仅到此而已,实际上呢是换汤不换药,对待测试方法构造的测试代码量一点也没有减少,方法还是那么的难测。
      解决的方法还是应该从根源找起,假如方法的代码很多,很可能就是因为类的职责不明确导致的,在一个类中干了多类该干的事,这时应该对类重新设计,明确职责,将功能进行分解。


 

 

 

 

 

 

 

 

 

 


 

0
0
分享到:
评论

相关推荐

    电感触摸资料——关于设计电感触摸的很好的资料和软件

    以下是对"电感触摸资料——关于设计电感触摸的很好的资料和软件"的详细解析: 1. **电感触摸原理**: 电感触摸技术基于电磁感应原理,系统由传感器阵列、控制器和电源组成。传感器阵列包含许多电感器,当手指或...

    免费 android 应用 源代码——记事本

    标题中的“免费 android 应用 源代码——记事本”表明这是一份关于Android应用开发的资源,特别是一个记事本应用的源代码。记事本应用是Android平台上常见的学习示例,它通常涉及到基础的用户界面设计、数据存储以及...

    android 应用 源代码——贪吃蛇

    【Android应用源代码——贪吃蛇】是一款基于Android平台的经典游戏,它展示了如何在移动设备上实现一个简单但趣味盎然的游戏程序。贪吃蛇游戏的原理是控制一条由多个身体部分组成的小蛇,在一个封闭的区域内移动,吃...

    iOS游戏应用源代码——domesticcatsoftware-DCFineTuneSlider.zip

    《iOS游戏应用源代码——domesticcatsoftware-DCFineTuneSlider详解》 在iOS开发领域,源代码是开发者理解并学习技术的关键。本篇将深入解析名为"domesticcatsoftware-DCFineTuneSlider"的项目源码,这是一个专为...

    iOS游戏应用源代码——peyton-MOOPullGesture.zip

    持续集成(Continuous Integration, CI)和自动化测试也可能被应用,以确保代码的质量和稳定性。 总之,“peyton-MOOPullGesture”源代码分析涵盖了iOS游戏开发的多个重要方面:手势识别、MVC架构、资源管理、以及...

    iOS游戏应用源代码——mikechambers-WoWTCGUtility-desktop.zip

    最后,为了确保代码的质量和可维护性,项目可能遵循了良好的编程规范,如使用注释来解释关键代码段的功能,遵循命名约定,以及使用单元测试和持续集成工具来验证代码的正确性。 总之,通过分析mikechambers-...

    iOS游戏应用源代码——jasarien-JSFavStarControl-86779b0.zip

    《iOS游戏应用源代码解析——JSFavStarControl》 在iOS开发中,游戏应用的源代码是开发者深入了解游戏机制、学习技术实现的关键资源。本文将深入探讨名为"jasarien-JSFavStarControl"的项目,这是一份基于iOS平台的...

    触摸屏flash源代码多点触摸旋转

    在本主题中,我们将深入探讨“触摸屏flash源代码”和“多点触摸旋转”这两个核心概念,以及如何通过“Simulator 虚拟器”进行测试。 首先,让我们来理解触摸屏Flash源代码。Flash是一种广泛用于创建交互式动画和...

    Android源码——城市列表特效-触摸查找源码.zip

    8. **代码组织与架构设计**: 一个良好的Android项目应该遵循MVC(模型-视图-控制器)或MVVM(模型-视图-ViewModel)等架构模式,使得代码可读性、可维护性更强。此外,源码中可能会有明确的包结构,分别存放模型、...

    iOS游戏应用源代码——mk12-Pong-Ultimate-9957f43.zip

    《iOS游戏应用源代码——mk12-Pong-Ultimate-9957f43.zip》是一款基于iOS平台的游戏开发资源,其中包含了名为“mk12-Pong-Ultimate-9957f43”的完整项目源代码。这款源代码是针对经典游戏"Pong"的一个终极版本实现,...

    iOS游戏应用源代码——yeag123-TextFightConcept-3c357c5.zip

    《iOS游戏应用源代码分析——yeag123-TextFightConcept》 本文将深入探讨一个基于iOS平台的游戏应用源代码——yeag123-TextFightConcept,它以3c357c5为特定版本。这个项目揭示了iOS游戏开发的核心技术、设计模式...

    iOS游戏应用源代码——atomton-ATMHud-fc79fed.zip

    最后,源代码中还可能包含了调试和测试的相关设施,如日志记录、断点、单元测试等,这些都是软件开发过程中的重要组成部分,确保了代码的质量和稳定性。 通过深入研究这个源代码,开发者可以学习到iOS游戏开发中的...

    iOS游戏应用源代码——eugenedvortsov-blackjack-89ed88b.zip

    《iOS游戏应用源代码分析——基于eugenedvortsov-blackjack项目》 在iOS开发领域,游戏应用的源代码是学习和理解移动游戏开发的关键资源。本篇将深入探讨"eugenedvortsov-blackjack-89ed88b.zip"这个压缩包中的源...

    iOS游戏应用源代码——mochidev-MDSpreadView-446a771.zip

    8. **代码组织与架构**:良好的代码结构和模块化设计是大型项目的基础,MDSpreadView的实现可能展示了如何组织代码以保持可维护性。 9. **持续集成与版本控制**:通过Git版本控制系统的使用,我们可以了解到如何协同...

    iOS游戏应用源代码——domesticcatsoftware-DCControls-722bb9c.zip

    《iOS游戏应用源代码解析——domesticcatsoftware-DCControls》 在iOS开发领域,源代码是理解应用程序工作原理和学习新技术的关键。本篇将详细探讨由domesticcatsoftware开发的DCControls库,该库专注于为iOS游戏...

    iOS游戏应用源代码——filipkunc-IronJump-d95b2d5.zip

    《iOS游戏应用源代码分析——基于filipkunc的IronJump》 在移动开发领域,iOS游戏应用一直是开发者关注的焦点。本篇文章将深入探讨一个名为"IronJump"的iOS游戏应用的源代码,该应用由filipkunc开发,并在特定版本...

    安卓Android源码——测试反应能力源码.zip

    这个"安卓Android源码——测试反应能力源码.zip"压缩包包含了用于测试用户反应速度的小游戏源代码,以及相关的图片资源,如"测试反应速度小游戏图片.jpg",可能是游戏的界面或者提示图标。另一个文件"MixedColor...

    iOS游戏应用源代码——ionine-Sauce-86a98eb.zip

    10. **测试与调试**:Xcode的内置工具如Instruments和单元测试框架可以帮助开发者调试代码,优化性能,确保游戏的质量和稳定性。 通过对源代码的深入剖析,我们可以学习到iOS游戏开发的多个层面,包括设计原则、...

    iOS游戏应用源代码——saumya-CatchThemAll-3166601.zip

    【iOS游戏应用源代码——saumya-CatchThemAll-3166601.zip】这个压缩包文件提供了一个完整的iOS游戏应用源代码,名为" Catch Them All"。这是一款可能受口袋妖怪启发的游戏,因为"catch them all"是口袋妖怪系列的...

    iOS游戏应用源代码——lolohouse-TextStepperField-a98518f.zip

    10. **单元测试与持续集成**:尽管未直接提及,但高质量的源代码通常会有对应的单元测试用例,以确保代码的正确性。可能还会有如XCTest或OCUnit这样的测试框架。 11. **版本控制**:文件名中的"a98518f"可能代表Git...

Global site tag (gtag.js) - Google Analytics