正如许多事情都有其两面性一样,测试方法也是这样。要保证测试方法正确,简单、直观地想法是多写些测试用例,从更多地角度去测试,但这必然增加我们的测试成本。小步快跑要求我们频繁进行测试,假如我们重构的周期是20分钟,但测试却要花掉10分钟,那么这样的成本实在太大了。假如这种测试还是开发人员手工测试,每天都有对同样的测试反复执行数十遍,那么开发人员估计要疯掉了。
  你可能立即想到自动化测试了。是的,在许多重构的书籍中,大师们都建议我们在重构开始前,首先建立自动化测试机制。但遗憾的是,我经过多年的实践总结出来的经验是,这几乎不可能实现。每次重构,我们面临的都是一个个遗留系统。大多数遗留系统都有一些共同的特征:代码凌乱,没有清晰的接口;代码间耦合度高,相互依赖严重;web层、业务层、数据访问层往往没有清晰的界限,代码相互参杂其中。在这样的情况下,编写自动化测试代码是几乎不可完成的任务。当然,这里所说的自动化测试代码,是指那些基于JUnit编写的自动化测试程序
  举一个简单的例子:假如你现在要测试一个开票类,想编写它的测试代码。本来这个开票类并不复杂,业务也很清晰。但是在函数传递参数时,其中一个参数是Web容器中的Request、Response或Session。这下麻烦了,为了测试一个简单的函数,我们必须启动整个Web应用,这是我们不可接受的。
  随后你可能会说了,我们为什么非要传递一个真正地Request、Response或Session呢?我们Mock一个假的嘛!想法不错,但你真正去尝试Mock时你会发现这也是一个不可完成的任务。Request、Response或Session有许多的状态,属性变量中又有对象,又有属性变量。除此还有大量集合变量,集合变量里都有什么对象,天才知道。因此,即使你费尽千辛万苦Mock出来,也可能因某些属性不对而使得测试失败。
  另一个写自动化测试程序比较忌讳的是访问数据库。比如你这次执行的插入操作成功了,并不意味着下次执行可以成功。下次执行会报“主键冲突”错误,出现这个错误并不是被测程序错了,而是测试程序错了。上次执行一个查询产生的结果集,不一定是下一次执行同样一个查询产生的结果。查询结果变了,并不意味着被测程序错了,而是测试程序不对。自动化测试程序之所以能够自动化执行,必须要保证测试过程是可以反复执行的,并且不论什么时候执行都有一个确定的结果。
  总之,自动化测试不是银弹,并不是所有代码都适合自动化测试。与Web容器或其它设备驱动相关的代码是不适合自动化测试的,因为我们在测试的时候不希望去启动Web容器或其它设备。因此,我们在做自动化测试程序前,首先应当确保要测试的程序已经与Web容器或其它设备驱动相关的代码充分解耦。一个比较好的办法是分离出Web层与BUS层,Web层负责从Web容器中获取数据,并打包传递给BUS层,而BUS层则完成真正需要测试的业务逻辑。
  另一个不适合自动化测试的是要访问数据库的程序,因为它们执行的结果总是与数据库状态有关,无法获得稳定而可以不断复现的结果。所以,我们解决它的好办法是将访问数据库的部分Mock掉。如何Mock呢?你不能Mock一个JDBC,也不能Mock一个Hibernate,因为那都过于复杂了,你可以做的是将DAO层Mock掉。这要求我们对系统重构的时候,要将数据库访问的代码从业务代码中脱离出来,写入到DAO层。后,被Mock的DAO层代码并不真正去访问数据库。每当客户程序传入一个参数时,它首先作为测试程序去验证这个参数是否与预期一致,然后返回一个确定的结果。