### JUnit测试框架
译注:本段将使用英文词:test-suite(测试套件)、test-case(测试用例)和test-fixture(测试装备),期能直接对应图4.1的JUnit结构组件,并有助于阅读JUnit文档。
我用的是JUnit,一个由Erich Gamma 和 Kent Beck [JUnit]开发的开放源码测试框架。这个框架非常简单,却可让你进行测试所需的所有重要事情。本章中我将运用这个测试框架来为一些IO classes幵发测试代码。
首先我创建一个FileReaderTester 来测试文件读取器。任何『包含测试代码」 的class都必须衍生自测试框架所提供的TestCase class。这个框架运用Composite 模式[Gang of Four],允许你将测试代码聚集到suites(套件)中,如图4.1。这些套件可以包含未加工的test-cases(测试用例),或其他test-suits(测试套件)。如此一来我就可以轻松地将一系列庞大的test-suits结合在一起,并自动运行它们。
![](https://box.kancloud.cn/2016-08-15_57b1b56b75cf7.gif)
图4.1 测试框架的Composite结构
~~~
class FileReaderTester extends TestCase {
public FileReaderTester (String name) {
super(name);
}
}
~~~
这个新建的class必须有一个构造函数。完成之后我就可以开始添加测试代码了。我的第一件工作是设置test fixture(测试装备),那是指「用于测试的对象样本」。由于我要读一个文件,所以先备妥一个测试文件如下:
~~~
Bradman
99.94
52
80
10
6996
334
29 Pollock
60.97
23
41
4
2256
274
7Headley
60.83
22
40
4
2256
270*
10Sutcliffe
60.73
54
84
9
4555
194
16
~~~
进一步运用这个文件之前,我得先准备好test-fixture (测试装备)。TestCase class 提供两个函数专门针对此一用途:setUp()用来产生相关对象、tearDown()负责删除它们。在TestCase class中这两个函数都只有空壳。大多数时候你不需要操心test-fixture的拆除(垃圾回收器会扛起责任),但是在这里,以tearDown()关闭文件无疑是明智之举:
~~~
class FileReaderTester...
protected void setUp() {
try {
_input = new FileReader("data.txt");
} catch (FileNotFoundException e) {
throw new RuntimeException ("unable to open test file");
}
}
protected void tearDown() {
try {
_input.close();
} catch (IOException e) {
throw new RuntimeException ("error on closing test file");
}
}
~~~
现在我有了适当的test-fixture(测试装备),可以开始编写测试代码了。首先要测试的是read(),我要读取一些字符,然后检查后续读取的字符是否正确:
~~~
public void testRead() throws IOException {
char ch = '&';
for (int i=0; i < 4; i++)
ch = (char) _input.read();
assert('d' == ch);
}
~~~
assert()扮演自动测试角色。如果assert()的参数值为ture,一切良好;否则我们就会收到错误通知。稍后我会让你看看测试框架怎么向用户报告错误消息。现在我要先介绍如何将测试过程运行起来。
第一步是产生一个test-suite(测试套件)。为达目的,请设计一个suite()如下:
~~~
class FileReaderTester...
public static Test suite() {
TestSuite suite= new TestSuite();
suite.addTest(new FileReaderTester("testRead"));
return suite;
}
~~~
这个测试套件只含一个test-case(测试用例)对象,那个是FileReaderTester实体。创建test-case对象时,我传给其构造函数一个字符串,这正是待测函数的名称。这会创建出一个对象,用以测试被指定的函数。这个测试系通过Java反射机制(reflection)和对象系结在一起。你可以自由下载JUnit源码,看看它究竟如何做到。至于我,我只把它当作一种魔法。
要将整个测试运行起来,还需要一个独立的TestRunner class。TestRunner 有两个版本,其中一个有漂亮的图形用户界面(GUI),另一个采用文字界面。我可以在main函数中调用「文字界面」版:
~~~
class FileReaderTester...
public static void main (String[] args) {
junit.textui.TestRunner.run (suite());
}
~~~
这段代码创建出一个TestRunner,并要它去测试FileReaderTester class。当我执行它,我看到:
~~~
.
Time: 0.110
OK (1 tests)
~~~
对于每个运行起来的测试,JUnit都会输出一个句点,这样你就可以直观看到测试进展。它会告诉你整个测试用了多长时间。如果所有测试都没有出错,它就会说"OK",并告诉你运行了多少笔测试。我可以运行上千笔测试,如果一切良好,我会看到那个"OK"。对于自我测试代码来说,这个简单的响应至关重要,没有它我就不可能经常运行这些测试。有了这个简单响应,你可以执行一大堆测试然后去吃个午饭(或幵个会),回来之后再看看测试结果。
TIP:频繁地运行测试。每次编译请把测试也考虑进去——每天至少执行毎个测试一次。
重构过程中,你可以只运行少数几项测试,它们主要用来检查当下正在开发或整理的代码。是的,你可以只运行少数几项测试,这样肯定比较快,否则整个测试会减低你的开发速度,使你开始犹豫是否还要这样下去。千万别屈服于这种诱惑,否则 你一定会付出代价。
如果测试出错,会发生什么事?为了展示这种情况,我故意放一只臭虫进去:
~~~
public void testRead() throws IOException {
char ch = '&';
for (int i=0; i < 4; i++)
ch = (char) _input.read();
assert('2' == ch); //deliberate error
}
~~~
得到如下结果:
~~~
.F
Time: 0.220
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 1 Errors: 0
There was 1 failure:
1) FileReaderTester.testRead
test.framework.AssertionFailedError
~~~
JUnit警告我测试失败,并告诉我这项失败具体发生在哪个测试身上。不过这个错误消息并不特别有用。我可以使用另一种形式的assert,让错误消息更清楚些:
~~~
public void testRead() throws IOException {
char ch = '&';
for (int i=0; i < 4; i++)
ch = (char) _input.read();
assertEquals('m',ch);
}
~~~
你做的绝大多数都是对两个值进行比较,检验它们是否相等,所以JUnit框架为你提供assertEquals()。这个函数很简单:以equals()进行对象比较,以操作符==进行数值比较——我自己常忘记区分它们。这个函数也输出更具意义的错误消息:
~~~
.F
Time: 0.170
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 1 Errors: 0
There was 1 failure:
1) FileReaderTester.testRead "expected:"m"but was:"d""
~~~
我应该提一下:编写测试代码时,我往往一开始先让它们失败。面对既有代码,要不我就修改它(如果我能碰触源码的话),使它测试失败,要不就在assertions中放一个错误期望值,造成测试失败。之所以这么做,是为了向自己证明:测试机制的确可以运行,并且的确测试了它该测试的东西(这就是为什么上面两种作法中我比较喜欢修改待测码的原因)。这可能有些偏执,或许吧,但如果测试代码所测的东西并非你想测的东西,你真的有可能被搞得迷迷糊糊。
除了捕捉「失败」(failures,也就是assertions之结果为"false"),JUnit还可以捕捉「错误」(errors,意料外的异常)。如果我关闭input stream,然后试图读取它,就应该得到一个异常(exception)。我可以这样测试:
~~~
public void testRead() throws IOException {
char ch = '&';
_input.close();
for (int i=0; i < 4; i++)
ch = (char) _input.read(); // will throw exception
assertEquals('m',ch);
}
~~~
执行上述测试,我得到这样的结果:
~~~
.E
Time: 0.110
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 0 Errors: 1
There was 1 error:
1) FileReaderTester.testRead
java.io.IOException: Stream closed
~~~
区分失败(failures)和错误(errors)是很有用的,因为它们的出现形式不同,排除的过程也不同。
JUnit还包含一个很好的图形用户界面(GUI,见图4.2)。如果所有测试都顺利通过,窗口下端的进度杆(progress bar )就呈绿色;如果有任何一个测试失败,进度杆就呈红色。你可以丢下这个不管,整个环境会自动将你在代码所做的任何修改连接(links)进来。这是一个非常方便的测试环境。
![](https://box.kancloud.cn/2016-08-15_57b1b56b8afe2.gif)
图4.2 JUnit的图形用户界面
**单元测试和功能测试**
JUnit框架的用途是单元测试,所以我应该讲讲单元测试和功能测试之间的差异。 我一直挂在嘴上的其实是「单元测试」,编写这些测试的目的是为了提高身为一个程序员的生产性能。至于让质保部门开心,那只是附带效果而已。单元测试是高度本地化(localized)的东西,每个test class只对单一package运作。它能够测试其他packages的接口,除此之外它将假设其他一切正常。
功能测试就完全不同。它们用来保证软件能够正常运作。它们只负责向客户提供质量保证,并不关心程序员的生产力。它们应该由一个喜欢寻找臭虫的个别团队来开发。这个团队应该使用重量级工具和技术来帮助自己开发良好的功能测试。
一般而言,功能测试尽可能把整个系统当作一个黑箱。面对一个GUI待测系统,它 们通过GUI来操作那个系统。面对文件更新程序或数据库更新程序,功能测试只观察特定输入所导致的数据变化。
一旦功能测试者或最终用户找到软件中的一只臭虫,要除掉它至少需要做两件事。 当然你必须修改代码,才得以排除错误,但你还应该添加一个单元测试,让它揭发这只臭虫。事实上,每当收到臭虫提报(bug report),我都首先编写一个单元测试,使这只臭虫浮现。如果需要缩小臭虫出没范围,或如果出现其他相关失败(failures),我就会编写不只一个测试。我使用单元测试来帮助我盯住臭虫,并确保我的单元测 试不会有类似的漏网之……呃……臭虫。
TIP:每当你接获臭虫提报(bug report),请先撰写一个单元测试来揭发这只臭虫。
JUnit框架被设计用来编写单元测试。功能测试往往以其他工具辅助进行,例如某些拥有GUI (图形用户界面)的测试工具,然而通常你还得撰写一些与你的应用程序息息相关的测试工具,使能够比单纯使用GUI scripts (脚本语言)更轻松地管理 test cases (测试用例)。你也可以运用JUnit来执行功能测试,但这通常不是最有效的形式。当我要进行重构时,我倚赖程序员的好朋友:单元测试。
- 译序 by 侯捷
- 译序 by 熊节
- 序言
- 前言
- 章节一 重构,第一个案例
- 起点
- 重构的第一步
- 分解并重组statement()
- 运用多态(Polymorphism)取代与价格相关的条件逻辑
- 结语
- 章节二 重构原则
- 何谓重构
- 为何重构
- 「重构」助你找到臭虫(bugs)
- 何时重构
- 怎么对经理说?
- 重构的难题
- 重构与设计
- 重构与性能(Performance)
- 重构起源何处?
- 章节三 代码的坏味道
- Duplicated Code(重复的代码)
- Long Method(过长函数)
- Large Class(过大类)
- Long Parameter List(过长参数列)
- Divergent Change(发散式变化)
- Shotgun Surgery(散弹式修改)
- Feature Envy(依恋情结)
- Data Clumps(数据泥团)
- Primitive Obsession(基本型别偏执)
- Switch Statements(switch惊悚现身)
- Parallel Inheritance Hierarchies(平行继承体系)
- Lazy Class(冗赘类)
- Speculative Generality(夸夸其谈未来性)
- Temporary Field(令人迷惑的暂时值域)
- Message Chains(过度耦合的消息链)
- Middle Man(中间转手人)
- Inappropriate Intimacy(狎昵关系)
- Alternative Classes with Different Interfaces(异曲同工的类)
- Incomplete Library Class(不完美的程序库类)
- Data Class(纯稚的数据类)
- Refused Bequest(被拒绝的遗贈)
- Comments(过多的注释)
- 章节四 构筑测试体系
- 自我测试代码的价值
- JUnit测试框架
- 添加更多测试
- 章节五 重构名录
- 重构的记录格式
- 寻找引用点
- 这些重构准则有多成熟
- 章节六 重新组织你的函数
- Extract Method(提炼函数)
- Inline Method(将函数内联化)
- Inline Temp(将临时变量内联化)
- Replace Temp with Query(以查询取代临时变量)
- Introduce Explaining Variable(引入解释性变量)
- Split Temporary Variable(剖解临时变量)
- Remove Assignments to Parameters(移除对参数的赋值动作)
- Replace Method with Method Object(以函数对象取代函数)
- Substitute Algorithm(替换你的算法)
- 章节七 在对象之间搬移特性
- Move Method(搬移函数)
- Move Field(搬移值域)
- Extract Class(提炼类)
- Inline Class(将类内联化)
- Hide Delegate(隐藏「委托关系」)
- Remove Middle Man(移除中间人)
- Introduce Foreign Method(引入外加函数)
- Introduce Local Extension(引入本地扩展)
- 章节八 重新组织数据
- Self Encapsulate Field(自封装值域)
- Replace Data Value with Object(以对象取代数据值)
- Change Value to Reference(将实值对象改为引用对象)
- Replace Array with Object(以对象取代数组)
- Replace Array with Object(以对象取代数组)
- Duplicate Observed Data(复制「被监视数据」)
- Change Unidirectional Association to Bidirectional(将单向关联改为双向)
- Change Bidirectional Association to Unidirectional(将双向关联改为单向)
- Replace Magic Number with Symbolic Constant(以符号常量/字面常量取代魔法数)
- Encapsulate Field(封装值域)
- Encapsulate Collection(封装群集)
- Replace Record with Data Class(以数据类取代记录)
- Replace Type Code with Class(以类取代型别码)
- Replace Type Code with Subclasses(以子类取代型别码)
- Replace Type Code with State/Strategy(以State/strategy 取代型别码)
- Replace Subclass with Fields(以值域取代子类)
- 章节九 简化条件表达式
- Decompose Conditional(分解条件式)
- Consolidate Conditional Expression(合并条件式)
- Consolidate Duplicate Conditional Fragments(合并重复的条件片段)
- Remove Control Flag(移除控制标记)
- Replace Nested Conditional with Guard Clauses(以卫语句取代嵌套条件式)
- Replace Conditional with Polymorphism(以多态取代条件式)
- Introduce Null Object(引入Null 对象)
- Introduce Assertion(引入断言)
- 章节十一 处理概括关系
- Pull Up Field(值域上移)
- Pull Up Method(函数上移)
- Pull Up Constructor Body(构造函数本体上移)
- Push Down Method(函数下移)
- Push Down Field(值域下移)
- Extract Subclass(提炼子类)
- Extract Superclass(提炼超类)
- Extract Interface(提炼接口)
- Collapse Hierarchy(折叠继承关系)
- Form Template Method(塑造模板函数)
- Replace Inheritance with Delegation(以委托取代继承)
- Replace Delegation with Inheritance(以继承取代委托)
- 章节十二 大型重构
- 这场游戏的本质
- Tease Apart Inheritance(梳理并分解继承体系)
- Convert Procedural Design to Objects(将过程化设计转化为对象设计)
- Separate Domain from Presentation(将领域和表述/显示分离)
- Extract Hierarchy(提炼继承体系)
- 章节十三 重构,复用与现实
- 现实的检验
- 为什么开发者不愿意重构他们的程序?
- 现实的检验(再论)
- 重构的资源和参考资料
- 从重构联想到软件复用和技术传播
- 结语
- 参考文献
- 章节十四 重构工具
- 使用工具进行重构
- 重构工具的技术标准(Technical Criteria )
- 重构工具的实用标准(Practical Criteria )
- 小结
- 章节十五 集成
- 参考书目