我摸、我摸、我摸摸摸——提高代码可测试性
虽然有了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方法在系统运行过程中根本不会使用,就只是为了让我们的代码好测,感觉是有点别扭。
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的实例。