怎样使用Junit Framework进行单元测试的编写 |
本帖源地址 http://www.ibm.com/developerworks/cn/java/l-junit/ 关于作者艾昂科技上海公司 2002 年 7 月 01 日
Junit附带文档所列举的单元测试带有一定的迷惑性,因为几乎所有的示例单元都是针对某个对象的某个方法, 似乎Junit的单元测试仅适用于类组织结构的静态约束,从而使初学者怀疑Junit下的单元测试所能带来的效果。 因此我们需要重新定义如何确定有价值的单元测试以及如何编写这些单元测试、维护这些单元测试,从而让 更多的程序员接受和熟悉Junit下的单元测试的编写。 在Junit单元测试框架的设计时,作者一共设定了三个总体目标,第一个是简化测试的编写,这种简化包括测 试框架的学习和实际测试单元的编写;第二个是使测试单元保持持久性;第三个则是可以利用既有的测试来 编写相关的测试。从这三个目标可以看出,单元测试框架的基本设计考虑依然是从我们现有的测试方式和方 法出发,而只是使测试变得更加容易实施和扩展并保持持久性。因此编写单元测试的原则可以从我们通常使 用的测试方法借鉴和利用。 在我们通常的测试中,一个单元测试一般针对于特定对象的一个特定特性,譬如,假定我们编写了一个针对 特定数据库访问的连接池的类包实现,我们会建立以下的单元测试:
这儿只列出了部分的可能测试,但是从这个列表我们可以看出单元测试的粒度。一个单元测试基本是以一个 对象的明确特性为基础,单元测试的过程应该限定在一个明确的线程范围内。根据上面所述,一个单元测试 的测试过程非常类似于一个Use Case的定义,但是单元测试的粒度一般来说比Use Case的定义要小,这点 是容易理解的,因为Use Case是以单独的事务单元为基础的,而单元测试是以一组聚合性很强的对象的特 定特征为基础的,一般而言一个事务中会利用许多的系统特征来完成具体的软件需求。 从上面的分析我们可以得出,测试单元应该以一个对象的内部状态的转换为基本编写单元。一个软件系统就 和一辆设计好的汽车一样,系统的状态是由同一时刻时系统内部的各个分立的部件的状态决定的,因此为了 确定一个系统最终的行为符合我们起始的要求,我们首先需要保证系统内的各个部分的状态会符合我们的设 计要求,所以我们的测试单元的重点应该放在确定对象的状态变换上。 然而需要注意的并不是所有的对象组特征都需要被编写成独立的测试单元,如何在对象组特征里筛选有价值 的测试单元的原则在JUnitTest Infected: Programmers Love Writing Tests一文中得到了正确的描述,你应 该在有可能引入错误的地方引入测试单元,通常这些地方存在于有特定边界条件、复杂算法以及需求变动比 较频繁的代码逻辑中。除了这些特性需要被编写成独立的测试单元外,还有一些边界条件比较复杂的对象方 法也应该被编写成独立的测试单元,这部分单元测试已经在Junit文档中被较好的描述和解释过了。 在基本确定了需要编写的单元测试,我们还应该问自己:编写好了这些测试,我们是否可以有把握地告诉自 己,如果代码通过了这些单元测试,我们能认定程序的运行是正确的,符合需求的。如果我们不能非常的确 定,就应该看看是否还有遗漏的需要编写的单元测试或者重新审视我们对软件需求的理解。通常来说,在开 始使用单元测试的时候,更多的单元测试总是没有错的。
一旦我们确定了需要被编写的测试单元,接下来就应该 在XP下强调单元测试必须由类包的编写者负责编写,这个限定对于我们设定的测试目标是必须的。因为只有 这样,测试才能保证对象的运行时态行为符合需求,而仅通过类接口的测试,我们只能确保对象符合静态约 束,因此这就要求我们在测试的过程中,必须开放一定的内部数据结构,或者针对特定的运行行为建立适当 的数据记录,并把这些数据暴露给特定的测试单元。这也就是说我们在编写单元测试时必须对相应的类包进行 修改,这样的修改也发生在我们以前使用的测试方法中,因此以前的测试标记及其他一些测试技巧仍然可以 在Junit测试中改进使用。
由于单元测试的总体目标是负责我们的软件在运行过程中的正确无误,因此在我们对一个对象编写单元测试的 时候,我们不但需要保证类的静态约束符合我们的设计意图,而且需要保证对象在特定的条件下的运行状态 符合我们的预先设定。还是拿数据库缓冲池的例子说明,一个缓冲池暴露给其他对象的是一组使用接口,其中 包括对池的参数设定、池的初始化、池的销毁、从这个池里获得一个数据连接以及释放连接到池中,对其他对 象而言随着各种条件的触发而引起池的内部状态的变化是不需要知道的,这一点也是符合封装原理的。但是池 对象的状态变化,譬如:缓存的连接数在某些条件下会增长,一个连接在足够长的运行后需要被彻底释放从 而使池的连接被更新等等,虽然外部对象不需要明确,但是却是程序运行正确的保证,所以我们的单元测试必 须保证这些内部逻辑被正确的运行。
编译语言的测试和调试是很难对运行的逻辑过程进行跟踪的,但是我们知道,无论逻辑怎么运行,如果状态 的转换符合我们的行为设定,那验证结果显然是正确的,因此在对一个对象进行单元测试的时候,我们需要对 多数的状态转换进行分析和对照,从而验证对象的行为。状态是通过一系列的状态数据来描述的,因此编写 单元测试首先分析出状态的变化过程(状态转换图对这个过程的描述非常清晰),然后根据状态的定义确定分 析的状态数据,最后是提供这些内部的状态数据的访问。在数据库连接池的例子中,我们对池实现的对象 DefaultConnectionProxy的状态变换进行分析后,我们决定把表征状态的OracleConnectionCacheImpl对象 公开给测试类。参见示例一
当单元测试完成后,我们可以用Junit提供的TestSuite对象对测试单元进行组织,你可以决定测试的顺序,然后运行你的测试。 通过上面的描述,我们对如何确定和编写测试有了基本的了解,但是需求总是变化的,因此我们的单元测试也会 根据需求的变化不断的演变。如果我们决定修改类的行为规则,可以明确的是,我们当然会对针对这个类的 测试单元进行修改,以适应变化。但是如果对这个类仅有调用关系的类的行为定义没有变化则相应的单元测 试仍然是可靠和充分的,同时如果包含行为变化的类的对象的状态定义与其没有直接的关系,测试单元仍然 起效。这种结果也是封装原则的优势体现。 |