随着Refactoring技术和XP软件工程技术的广泛推广单元测试的作用在软件工程中变得越来越重要而一个简明易学适用广泛高效稳定的单元测试框架则对成功的实施单元测试有着至关重要的作用在java编程语句环境里Junit Framework是一个已经被多数java程序员采用和实证的优秀的测试框架但是多数没有尝试Junit Framework的程序员在学习如何Junit Framework来编写适应自己开发项目的单元测试时依然觉得有一定的难度这可能是因为Junit随框架代码和实用工具附带的用户指南和文档的着重点在于解释单元测试框架的设计方法以及简单的类使用说明而对在特定的测试框架(Junit)下如何实施单元测试如何在项目开发的过程中更新和维护已经存在的单元测试代码没有详细的解释因此本文档就两个着重点对Junit所附带的文档进行进一步的补充和说明使Junit能被更多的开发团队采用让单元测试乃至RefactoringXP技术更好在更多的开发团队中推广
单元测试的编写原则
Junit附带文档所列举的单元测试带有一定的迷惑性因为几乎所有的示例单元都是针对某个对象的某个方法似乎Junit的单元测试仅适用于类组织结构的静态约束从而使初学者怀疑Junit下的单元测试所能带来的效果因此我们需要重新定义如何确定有价值的单元测试以及如何编写这些单元测试维护这些单元测试从而让更多的程序员接受和熟悉Junit下的单元测试的编写
在Junit单元测试框架的设计时作者一共设定了三个总体目标第一个是简化测试的编写这种简化包括测试框架的学习和实际测试单元的编写第二个是使测试单元保持持久性第三个则是可以利用既有的测试来编写相关的测试从这三个目标可以看出单元测试框架的基本设计考虑依然是从我们现有的测试方式和方法出发而只是使测试变得更加容易实施和扩展并保持持久性因此编写单元测试的原则可以从我们通常使用的测试方法借鑒和利用
如何确定单元测试
在我们通常的测试中一个单元测试一般针对于特定对象的一个特定特性譬如假定我们编写了一个针对特定数据库访问的连接池的类包实现我们会建立以下的单元测试
在连接池启动后是否根据定义的规则在池中建立了相应数量的数据库连接
申请一个数据库连接是否根据定义的规则从池中直接获得缓存连接的引用还是建立新的连接
释放一个数据库连接后连接是否根据定义的规则被池释放或者缓存以便以后使用
后台Housekeeping线程是否按照定义的规则释放已经过期的连接申请
如果连接有时间期限后台Housekeeping线程是否定期释放已经过期的缓存连接
这儿只列出了部分的可能测试但是从这个列表我们可以看出单元测试的粒度一个单元测试基本是以一个对象的明确特性为基础单元测试的过程应该限定在一个明确的线程范围内根据上面所述一个单元测试的测试过程非常类似于一个Use Case的定义但是单元测试的粒度一般来说比Use Case的定义要小这点是容易理解的因为Use Case是以单独的事务单元为基础的而单元测试是以一组聚合性很强的对象的特定特征为基础的一般而言一个事务中会利用许多的系统特征来完成具体的软件需求
从上面的分析我们可以得出测试单元应该以一个对象的内部状态的转换为基本编写单元一个软件系统就和一辆设计好的汽车一样系统的状态是由同一时刻时系统内部的各个分立的部件的状态决定的因此为了确定一个系统最终的行为符合我们起始的要求我们首先需要保证系统内的各个部分的状态会符合我们的设计要求所以我们的测试单元的重点应该放在确定对象的状态变换上
然而需要注意的并不是所有的对象组特征都需要被编写成独立的测试单元如何在对象组特征里筛选有价值的测试单元的原则在JUnitTest Infected: Programmers Love Writing Tests一文中得到了正确的描述你应该在有可能引入错误的地方引入测试单元通常这些地方存在于有特定边界条件复杂算法以及需求变动比较频繁的代码逻辑中除了这些特性需要被编写成独立的测试单元外还有一些边界条件比较复杂的对象方法也应该被编写成独立的测试单元这部分单元测试已经在Junit文档中被较好的描述和解释过了
在基本确定了需要编写的单元测试我们还应该问自己编写好了这些测试我们是否可以有把握地告诉自己如果代码通过了这些单元测试我们能认定程序的运行是正确的符合需求的如果我们不能非常的确定就应该看看是否还有遗漏的需要编写的单元测试或者重新审视我们对软件需求的理解通常来说在开始使用单元测试的时候更多的单元测试总是没有错的
一旦我们确定了需要被编写的测试单元接下来就应该
如何编写单元测试
在XP下强调单元测试必须由类包的编写者负责编写这个限定对于我们设定的测试目标是必须的因为只有这样测试才能保证对象的运行时态行为符合需求而仅通过类接口的测试我们只能确保对象符合静态约束因此这就要求我们在测试的过程中必须开放一定的内部数据结构或者针对特定的运行行为建立适当的数据记录并把这些数据暴露给特定的测试单元这也就是说我们在编写单元测试时必须对相应的类包进行修改这样的修改也发生在我们以前使用的测试方法中因此以前的测试标记及其他一些测试技巧仍然可以在Junit测试中改进使用
由于单元测试的总体目标是负责我们的软件在运行过程中的正确无误因此在我们对一个对象编写单元测试的时候我们不但需要保证类的静态约束符合我们的设计意图而且需要保证对象在特定的条件下的运行状态符合我们的预先设定还是拿数据库缓沖池的例子说明一个缓沖池暴露给其他对象的是一组使用接口其中包括对池的参数设定池的初始化池的销毁从这个池里获得一个数据连接以及释放连接到池中对其他对象而言随着各种条件的触发而引起池的内部状态的变化是不需要知道的这一点也是符合封装原理的但是池对象的状态变化譬如缓存的连接数在某些条件下会增长一个连接在足够长的运行后需要被彻底释放从而使池的连接被更新等等虽然外部对象不需要明确但是却是程序运行正确的保证所以我们的单元测试必须保证这些内部逻辑被正确的运行
编译语言的测试和调试是很难对运行的逻辑过程进行跟蹤的但是我们知道无论逻辑怎么运行如果状态的转换符合我们的行为设定那验证结果显然是正确的因此在对一个对象进行单元测试的时候我们需要对多数的状态转换进行分析和对照从而验证对象的行为状态是通过一系列的状态数据来描述的因此编写单元测试首先分析出状态的变化过程(状态转换图对这个过程的描述非常清晰)然后根据状态的定义确定分析的状态数据最后是提供这些内部的状态数据的访问在数据库连接池的例子中我们对池实现的对象DefaultConnectionProxy的状态变换进行分析后我们决定把表征状态的OracleConnectionCacheImpl对象公开给测试类参见示例一
示例一
/*** 这个类简单的包装了oracle对数据连接缓沖池的实现**/public class DefaultConnectionProxy extends ConnectionProxy {private static final String name = Default Connection Proxy;private static final String description = 这个类简单的包装了oracle对数据连接缓沖池的实现;private static final String author = ;private static final int major_version = ;private static final int minor_version = ;private static final boolean pooled = true;private ConnectionBroker connectionBroker = null;private Properties props;private Properties propDescriptions; >private Object initLock = new Object();// Test Code Begin/* 为了能够了解对象的状态变化因此需要把表征对象内部状态变化的部分私有变量提供公共的访问接口(或者提供让同一个类包的访问接口)以便使测试单元可以有效地判断对象的状态转变在本示例中对包装的OracleConnectionCacheImpl对象提供访问接口*/OracleConnectionCacheImpl getConnectionCache() {if (connectionBroker == null) {throw new IllegalStateException(You need start the server first);}return connectionBrokergetConnectionCache();}
<
// Test Code End
在公开内部状态数据后我们就可以编写我们的测试单元了单元测试的选择方法和选择尺度已经在本文前面章节进行了说明
但是仍然需要注意的是由于assert方法会抛出一个error你应该在测试方法的最后集中用assert相关方法进行判断 这样可以确保资源得到释放
对数据库连接池的例子我们可以建立测试类DefaultConnectionProxyTest同时建立数个test case如下
示例二
/*** 这个类对示例一中的类进行简单的测试**/public class DefaultConnectionProxyTest extends TestCase {private DefaultConnectionProxy conProxy = null;private OracleConnectionCacheImpl cacheImpl = null;private Connection con = null;/** 设置测试的fixture建立必要的测试起始环境*/protected void setUp() {conProxy = new DefaultConnectionProxy();conProxystart();cacheImpl = conProxygetConnectionCache();}/** 对示例一中的对象进行服务启动后的状态测试检查是否在服务启动后连接池的参数设置是否正确*/public void testConnectionProxyStart() {int minConnections = ;int maxConnections = ;assertNotNull(cacheImpl);try{minConnections = IntegerparseInt(PropertyManagergetProperty(DefaultConnectionProxyminConnections));maxConnections = IntegerparseInt(PropertyManagergetProperty(DefaultConnectionProxymaxConnections));} catch (Exception e) {// ignore the exception}assertEquals(cacheImplgetMinLimit() minConnections);assertEquals(cacheImplgetMaxLimit() maxConnections);assertEquals(cacheImplgetCacheSize() minConnections);}/** 对示例一中的对象进行获取数据库连接的测试看看是否可以获取有效的数据库连接并且看看获取连接后连接池的状态是否按照既定的策略进行变化由于assert方法抛出的是error对象因此尽可能把assert方法放置到方法的最后集体进行测试这样在方法内打开的资源才能有效的被正确关闭*/public void testGetConnection() {int cacheSize = cacheImplgetCacheSize();int activeSize = cacheImplgetActiveSize();int cacheSizeAfter = ;int activeSizeAfter = ;con = conProxygetConnection();>if (con != null) {activeSizeAfter = cacheImplgetActiveSize();cacheSizeAfter = cacheImplgetCacheSize();try{conclose();} catch (SQLException e) {}} else {assertNotNull(con);}/*如果连接池中的实际使用连接数小于缓存连接数检查获取的新的数据连接是否从缓存中获取反之连接池是否建立新的连接*/if (cacheSize > activeSize){assertEquals(activeSize + activeSizeAfter);assertEquals(cacheSize cacheSizeAfter);} else {assertEquals(activeSize + cacheSizeAfter);}}/** 对示例一中的对象进行数据库连接释放的测试看看连接释放后连接池的状态是否按照既定的策略进行变化由于assert方法抛出的是error对象因此尽可能把assert方法放置到方法的最后集体进行测试这样在方法内打开的资源才能有效的被正确关闭*/public void testConnectionClose() {int minConnections = cacheImplgetMinLimit();int cacheSize = ;int activeSize = ;int cacheSizeAfter = ;int activeSizeAfter = ;con = conProxygetConnection();if (con != null) {cacheSize = cacheImplgetCacheSize();activeSize = cacheImplgetActiveSize();try{conclose();} catch (SQLException e) {}activeSizeAfter = cacheImplgetActiveSize();cacheSizeAfter = cacheImplgetCacheSize();} else {assertNotNull(con);}assertEquals(activeSize activeSizeAfter + );/*如果连接池中的缓存连接数大于最少缓存连接数检查释放数据连接后是否缓存连接数比之前减少了一个反之缓存连接数是否保持为最少缓存连接数*/if (cacheSize > minConnections){assertEquals(cacheSize cacheSizeAfter + );} else {assertEquals(cacheSize minConnections);}}/** 释放建立测试起始环境时的资源*/protected void tearDown() {cacheImpl = null;conProxydestroy();}public DefaultConnectionProxyTest(String name) {super(name);}/** 你可以简单的运行这个类从而对类中所包含的测试单元进行测试*/public static void main(String args[]) {junittextuiTestRunnerrun(DefaultConnectionProxyTestclass);}}
当单元测试完成后我们可以用Junit提供的TestSuite对象对测试单元进行组织你可以决定测试的顺序然后运行你的测试
如何维护单元测试
通过上面的描述我们对如何确定和编写测试有了基本的了解但是需求总是变化的因此我们的单元测试也会根据需求的变化不断的演变如果我们决定修改类的行为规则可以明确的是我们当然会对针对这个类的测试单元进行修改以适应变化但是如果对这个类仅有调用关系的类的行为定义没有变化则相应的单元测试仍然是可靠和充分的同时如果包含行为变化的类的对象的状态定义与其没有直接的关系测试单元仍然起效这种结果也是封装原则的优势体现