DRINKING WITH JUNIT
作者: 乐晨光 [airwalker@263.net] (欢迎自由转载本文,但请注明作者及出处)
安全合理而富有可扩展性与可读性的测试代码进入了对象中,它就象一棵大数那样支撑起了整个工程。
1. PURPOSE
u 理解系统测试与单元测试的目的与方式
u 考察Junit测试框架的客户层应用;并架构一个待测试的系统案例。
u 考察分析Junit的框架架构原理。
2. THE TESTING
2.1 软件熵(software entropy)的概念:一个程序从设计很好的状态开始,随着新的功能不断地加入,程序逐渐地失去了原有的结构,最终变成了一团乱麻。(其实该处的“很好的状态”得加个?)
2.2 测试的目的:简单而极具活力,写出高质量的软件并解决软件熵这一问题。
2.3 测试的分类:白箱测试、黑箱测试;或,单元测试、集成测试、功能测试等有多种分类方法。
白箱测试 -> 指知道被测试的软件如何(how)完成功能测试和完成什么样的功能(what)的条件下所作的测试。(一般由开发人员完成)
单元测试应属于白箱测试。
黑箱测试 -> 在知道被测试的软件完成什么样(what)的功能的条件下所作的测试。(一般由测试人员完成)
单元测试 -> 目的为验证一个或若干个类是否按所设计那样正常工作与表现。
集成测试 -> 验证所有的类是否可以相互配合,协同完成特定的任务。(复杂的信号与事件通讯与状态机密集大的运作测试)
<<<< Junit应用于单元测试 >>>>
2.4 测试作为一项持续的过程,它贯穿与开发的整个过程中,单元测试尤适合迭代增量式(iteraton and incremental)的开发过程。
最简单的说:测试就是比较预期结果是否与实际执行结果一致。
3.ARCHITECTURE OF JUNIT
3.1 Goal of JUnit
u 创建一个通用的测试框架,将测试代码封装入对象中,从而使开发者乐于同步设计并配置自己的单元测试。
u 让测试代码具备保值性,使(以往是个人化的)测试代码的进行方式标准化,可为多人共同浏览、维护。
u 将测试代码从系统代码中剥离开,二者可同步发展。
3.2 JUnit的框架成员逻辑分析
1. 被测试的对象(类,多个类,子系统)(Object be Tested)
2. 对测试目标进行测试的方法与过程集合,可将其称为测试容器。(TestCase)
3. 测试事务的集合,可容纳多个测试容器(TestCase),将其称作测试事务包。(TestSuite)
4. 测试结果的描述与记录。(TestResult)
5. 每一个测试方法所发生的与预期不一致状况的描述,称其测试失败元素。(TestFailure)
6. JUnit Framework中的出错异常。(AssertionFailedError)
3.3 JUnit框架功能以及原理描述(junit.framework.*)
u Test接口与TestCase、TestSuite形成了Composite结构,Run(TestResult) 则是Composite Methode。Test为Component,派生出的TestCase为Leaf,是测试的执行元素;TestSuite为Composite,其可通过addTest(Test)来容纳Test组合(TestCase or TestSuite)形成测试包。
u TestCase可在框架中视为测试单元的运行实体。用户可以通过它派生自定义的测试过程与方式(单元),利用Command Pattern与Composite Pattern使其形成可组合装配的可扩展的测试批处理。TestCase本身的运作操作为run(TestResult),其中分别执行setUp(),runTest(),tearDown()来架构测试过程。Template Mothod Pattern使用户无须了解执行框架的过程细节,而只需重定义特性化的测试预处理,测试单元过程,以及测试完毕测试三个Template Mothod就能使测试正确工作。
u 用户层可通过Java的匿名内部类(Anonymous Inner Classes)来集成化重载runTest()形成特性测试类,同时JUnit3.0版本以后则支持利用Java的Class属性来动态框架后台生成这些特性测试单元类,而用户只需在TestCase派生类的testXXX()即可,在suite中使用return new TestSuite(MyTestCase.class);后该TestSuite就包含了所有testXXX为测试过程的TestCase类对象集合。
u Assert类包含了assertEqual(..),assertSame(..),assertTrue(.)等静态工具方法(static tools methods),为使用户对系统所了解的类型尽可能少,JUnit框架将Assert作为了TestCase的超类,TestCase同时继承了Assert的实现与Test接口(class Adepter pattern)。用户可在TestCase中直接调用这些assertXXX(..)等静态工具方法。
u TestResult描述了整个测试执行过程的测试结果,JUnit框架将其作为参数传递与Test的run(.)间,当测试任务执行完毕后,TestResult内则包含了所有TestCase的测试结果。框架默认的TestResult为TextTestResult,用户也可以自定义其TestResult的回显格式,譬如HTMLTestResult。
TestFailure描述了测试过程中的错误信息,TestResult通过Vector容纳了测试过程中生成的TestFailure,将其作为测试结果参数依据。
4. CLIIENT LAYER OF JUNIT
Import junit.framework.*;
用户通过派生TestCase来自定义特性Test案例,在TestCase中可包含系统待测试类型的对象,同时在其内定义若干testXXX()做test method。
覆盖setUp()来为每一次测试前做系统状态预处理,在tearDown()中清理。
安排测试过程有两种常用的方法:
1. 通过创建内部类来集成化覆盖runTest()方法,里边填充testXXX()
2. 通过class作为TestSuite的构造参数来动态创建测试包。( 其中所有的testXXX()均分别作为了一个新的特性TestCase的测试主方法[runTest{testXXX();}中] )
注意每一次测试(runTest)都应该相互是相互独立的,被测试对象不应该有上下文关系。(这是由于TestSuite容纳Test的vector的无序性)
5. TEST ZERG-COMMAND-SYSTEM WITH JUNIT
作为JUnit的测试案例,我架构了一个测试案例。
JUnit作为一个单元测试框架,开发者应该明确创设什么样的测试,如何测试,测试系统的什么部分以及方式,或是测试系统的整合表现。
该系统描述的是一个外星种族Zerg之星球作战指令系统。
ZCommand系统是一个Command & Composite架构,ZcommandGroup容纳若干命令(可视为批处理),ZconcreteCommand(ZCFire/ZCLoadAmmo/ZCAction)描述了3种具体命令类别(包含具体特性参数)
Zcommand的execute是一个composite method,可处理组合命令的批量执行。
架构这个系统的同时,要利用JUnit框架来测试该组合的性质。考察该命令系统,算法复杂区域表现在组合集成的过程里,以及执行组合执行的遍历过程。
public class TestCommandModel extends TestCase {}1. testCommandGroupCount() 测试一份组合命令所包含的子命令数目。
2. testCommandString() 测试组合命令执行的遍历回显过程。
3. testCommandGroupUpdate() 测试add, remove, clear等组合操作的正确性。
(详见junitzerg.test.TestCommandModel.java)
ZfighterMachine描述了一架作战指挥终端。ZergCommandCentre描述了Zerg作战指挥处理中心,ZfighterMachine将作战人员生成的Zcommand(component)传递给ZcommandCentre(remote object) 来处理该指令并等待一份执行结果。
ZcommandCentre维护了个系统的工作状态,不同的作战指令(Zcommand)在不同的系统工作状态下有着不同的处理方式,这一套系统规则形成了一个复杂的逻辑区(complex logic zone)
该逻辑区的信号传入与结果反馈正应为单元测试应实行的内容。故价构了TestMachine来测试ZfighterMachine与ZergCommandCentre的工作状态。
public class TestMachine extends TestCase {}
1. testSimpleCommand() 测试简单指令元素在不同系统状态下的执行结果。
2. testExtremeCondition() 测试ZergCommandCentre在极端环境下(能源不足、弹药不足、通讯干扰)的工作情况。
3. testAllCondition() 则测试所有种类的Command在所有condition下的执行结果与工作状况。
(备注:详见junitzerg.test.TestMachine.java,TestAll则批处理执行所有的测试)
6. SUMMARY & CONCLUSION
1.设计一个应当失败的测试。
2.勿用TestCase的Constructor初始化Fixtures,用setUp/tearDown。
3. 避免编写有副作用的TestCase。
4. 确保测试与时间无关,莫依赖使用过期的数据进行测试。
5. 尽可能利用JUnit提供的assertXXX/fail方法以及异常处理,使测试代码统一化。
6. 如果编写的软件面向国际市场,则编写测试时要考虑国际化的因素。
7. 无意间发现JUnit的作者包括Erich Gamma。
7. EXTENSION
1. 测试的关键是洞悉系统的复杂区域与不稳定区域,抽取其做测试。
2. 管理测试模块与系统本身模块,使其同步发展。
3. JUnit仅为一份单元测试的应用框架,集成测试的框架又会是何其。
4. 考察总结开发与测试并同的工作模式,以加快工程开发的运行效率。
乐晨光 2002-2-26