JUnit 4.4 新特性
随着当前 Java 开发的越发成熟,Agile 和 TDD 的越发流行,自动化测试的呼声也越来越高。若想将单元测试变得自动化,自然 JUnit 这把利器必不可少,这也是 JUnit 自 1997 年诞生以来在 Java 开发业界一直相当流行的原因。
JUnit 是针对 Java 语言的一个单元测试框架,它被认为是迄今为止所开发的最重要的第三方 Java 库。 JUnit 的优点是整个测试过程无需人的参与,无需分析和判断最终测试结果是否正确,而且可以很容易地一次性运行多个测试。 JUnit 的出现促进了测试的盛行,它使得 Java 代码更健壮,更可靠,Bug 比以前更少。
JUnit 自从问世以来一直在不停的推出新版本,目前最新的版本是 2007 年 7 月发布的 JUnit 4.4,它是继 JUnit4 以来最大的发行版,提供了很多有用的新特性。本文将假设读者已经具有 JUnit 4 的使用经验。
JUnit 设计的目的就是有效地抓住编程人员写代码的意图,然后快速检查他们的代码是否与他们的意图相匹配。 JUnit 发展至今,版本不停的翻新,但是所有版本都一致致力于解决一个问题,那就是如何发现编程人员的代码意图,并且如何使得编程人员更加容易地表达他们的代码意 图。JUnit 4.4 也是为了如何能够更好的达到这个目的而出现的。
JUnit 4.4 主要提供了以下三个大方面的新特性来更好的抓住编程人员的代码意图:
- 提供了新的断言语法(Assertion syntax)——
assertThat
- 提供了假设机制(Assumption)
- 提供了理论机制(Theory)
新的断言语法(Assertion syntax)—— assertThat
JUnit 4.4 学习 JMock,引入了 Hamcrest 匹配机制,使得程序员在编写单元测试的 assert
语句时,可以具有更强的可读性,而且也更加灵活。
Hamcrest 是一个测试的框架,它提供了一套通用的匹配符
Matcher,灵活使用这些匹配符定义的规则,程序员可以更加精确的表达自己的测试思想,指定所想设定的测试条件。比如,有时候定义的测试数据范围太精
确,往往是若干个固定的确定值,这时会导致测试非常脆弱,因为接下来的测试数据只要稍稍有变化,就可能导致测试失败(比如 assertEquals( x, 10 );
只能判断 x
是否等于 10
,如果 x
不等于 10
,
测试失败);有时候指定的测试数据范围又不够太精确,这时有可能会造成某些本该会导致测试不通过的数据,仍然会通过接下来的测试,这样就会降低测试的价
值。 Hamcrest 的出现,给程序员编写测试用例提供了一套规则和方法,使用其可以更加精确的表达程序员所期望的测试的行为。(具体
Hamcrest 的使用,请参阅 参考资料
)
JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat
。程序员可以只使用 assertThat
一个断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。
assertThat
的基本语法如下:
|
-
value
是接下来想要测试的变量值; -
matcher statement
是使用 Hamcrest 匹配符来表达的对前面变量所期望的值的声明,如果value
值与matcher statement
所表达的期望值相符,则测试成功,否则测试失败。
-
优点 1
:以前 JUnit 提供了很多的 assertion 语句,如:
assertEquals
,assertNotSame
,assertFalse
,assertTrue
,assertNotNull
,assertNull
等,现在有了 JUnit 4.4,一条assertThat
即可以替代所有的 assertion 语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护。 -
优点 2
:
assertThat
使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活。如清单 2 所示:
清单 2 使用匹配符 Matcher 和不使用之间的比较
// 想判断某个字符串 s 是否含有子字符串 "developer" 或 "Works" 中间的一个
// JUnit 4.4 以前的版本:assertTrue(s.indexOf("developer")>-1||s.indexOf("Works")>-1 );
// JUnit 4.4:
assertThat(s, anyOf(containsString("developer"), containsString("Works")));
// 匹配符 anyOf 表示任何一个条件满足则成立,类似于逻辑或 "||", 匹配符 containsString 表示是否含有参数子
// 字符串,文章接下来会对匹配符进行具体介绍
-
优点 3
:
assertThat
不再像assertEquals
那样,使用比较难懂的“谓宾主”语法模式(如:assertEquals(3, x);
),相反,assertThat
使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3));
),使得代码更加直观、易读。 -
优点 4
:可以将这些 Matcher 匹配符联合起来灵活使用,达到更多目的。如清单 3 所示:
清单 3 Matcher 匹配符联合使用
// 联合匹配符not 和equalTo 表示“不等于”
assertThat( something, not( equalTo( "developer" ) ) );
// 联合匹配符not 和containsString 表示“不包含子字符串”
assertThat( something, not( containsString( "Works" ) ) );
// 联合匹配符anyOf 和containsString 表示“包含任何一个子字符串”
assertThat(something, anyOf(containsString("developer"), containsString("Works"))); -
优点 5
:错误信息更加易懂、可读且具有描述性(descriptive)。
JUnit 4.4 以前的版本默认出错后不会抛出额外提示信息,如:
assertTrue( s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
如果该断言出错,只会抛出无用的错误信息,如:
junit.framework.AssertionFailedError:null
。如果想在出错时想打印出一些有用的提示信息,必须得程序员另外手动写,如:
assertTrue( "Expected a string containing 'developer' or 'Works'",
s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
非常的不方便,而且需要额外代码。
JUnit 4.4 会默认自动提供一些可读的描述信息,如清单 4 所示:
清单 4 JUnit 4.4 默认提供一些可读的描述性错误信息
String s = "hello world!";
assertThat( s, anyOf( containsString("developer"), containsString("Works") ) );
// 如果出错后,系统会自动抛出以下提示信息:
java.lang.AssertionError:
Expected: (a string containing "developer" or a string containing "Works")
got: "hello world!"
- 优点 6 :开发人员可以通过实现 Matcher 接口,定制自己想要的匹配符。当开发人员发现自己的某些测试代码在不同的测试中重复出现,经常被使用,这时用户就可以自定义匹配符,将这些代码绑定在一个 断言语句中,从而可以达到减少重复代码并且更加易读的目的。(具体怎么实现自定义可配置的匹配符,请参阅参考资料 )
JUnit 4.4 自带了一些 Hamcrest 的匹配符 Matcher,但是只有有限的几个,在类 org.hamcrest.CoreMatchers
中定义,要想使用他们,必须导入包 org.hamcrest.CoreMatchers.*
。
如果想使用一些其他更多的匹配符 Matcher,可以从 Hamcrest 网页下载 hamcrest-library-1.1.jar
和 hamcrest-core-1.1.jar
(请参阅 参考资料
),并将其加入到工程库中,所有的匹配符都在类 org.hamcrest.Matchers
中定义,要想使用,必须得在代码中 import static org.hamcrest.Matchers.*;
。如果使用外部的匹配符,最好就不要再使用 JUnit 4.4 自带的匹配符了,因为这样容易导致匹配符 Matcher 重复定义,编译可能会出错(ambiguous for the type
)。 JUnit 4.4 允许使用 Hamcrest 来使用更多的匹配符,这还是 JUnit 第一次允许在自己的工程中使用第三方类。
注意:
-
assertThat
仍然是断言语句,所以要想使用,必须还得import static org.junit.Assert.*;
; - 虽然
assertThat
可以代替以前所有的断言语句,但是以前的所有 assert 语句仍然可以继续使用;
清单 5 列举了大部分 assertThat
的使用例子:
|
理想情况下,写测试用例的开发人员可以明确的知道所有导致他们所写的测试用例不通过的地方,但是有的时候,这些导致测试用例不通过的地方并不 是很容易的被发现,可能隐藏得很深,从而导致开发人员在写测试用例时很难预测到这些因素,而且往往这些因素并不是开发人员当初设计测试用例时真正目的,他 们的测试点是希望测试出被测代码中别的出错地方。
比如,一个测试用例运行的 locale(如:Locale.US
)与之前开发人员设计该测试用例时所设想的不同(如:Locale.UK
),这样会导致测试不通过,但是这可能并不是开发人员之前设计测试用例时所设想的测试出来的有用的失败结果(测试点并不是此,比如测试的真正目的是想判断函数的返回值是否为 true
,返回 false
则测试失败),这时开发人员可以通过编写一些额外的代码来消除这些影响(比如将 locale
作为参数传入到测试用例中,每次运行测试用例时,明确指定
locale),但是花费时间和精力来编写这些不是测试用例根本目的的额外代码其实是种浪费,这时就可以使用 Assumption
假设机制来轻松达到额外代码的目的。编写该测试用例时,首先假设 locale 必须是 Locale.UK
,如果运行时 locale 是 Locale.UK
,则继续执行该测试用例函数,如果是其它的 locale,则跳过该测试用例函数,执行该测试用例函数以外的代码,这样就不会因为 locale 的问题导致测试出错。
JUnit 4.4 结合 Hamcrest 库提供了 assumeThat
语句,开发人员可以使用其配合匹配符 Matcher
设计所有的假设条件(语法和 assertThat
一样)。同样为了方便使用,JUnit 4.4 还专门提供了 assumeTrue
,assumeNotNull
和 assumeNoException
语句。
-
优点 1 :通过对 runtime 变量进行取值假设,从而不会因为一个测试用例的不通过而导致整个测试失败而中断(the test passes),使得测试更加连贯。
开发人员编写单元测试时,经常会在一个测试中包含若干个测试用例函数,这时若是遇到某个测试用例函数不通过,整个单元测试就会终止。这将 导致测试不连贯,因为开发人员往往希望一次能运行多个测试用例函数,不通过的测试用例函数不要影响到剩下的测试用例函数的运行,否则会给 debug 调试带来很大的难度。
开发人员编写单元测试时,有时是预测不了传入到单元测试方法中的变量值的,而且这些值有时并不是开发人员所期望的,因为他们会导致测试用例不通过并中断整个测试,所以开发人员需要跳过这些导致测试用例函数不通过的异常情况。
清单 6 假设机制优点 1 举例
//@Test 注释表明接下来的函数是 JUnit4 及其以后版本的测试用例函数
@Test
public void testAssumptions() {
//假设进入testAssumptions时,变量i的值为10,如果该假设不满足,程序不会执行assumeThat后面的语句
assumeThat( i, is(10) );
//如果之前的假设成立,会打印"assumption is true!"到控制台,否则直接调出,执行下一个测试用例函数
System.out.println( "assumption is true!" );
}
-
优点 2 :利用假设可以控制某个测试用例的运行时间,让其在自己期望的时候运行(run at a given time)。
清单 7 假设机制优点 2 举例
@Test
//测试用例函数veryLongTest()执行需要很长时间,所以开发人员不是每次都想运行它,可以通过判断是否定义了
//”DEV”环境变量来选择性地执行该测试用例
public void veryLongTest() throws Exception {
//假设环境变量”DEV”为空,即如果之前通过System.setProperty定义过”DEV”环境变量(不为空),则自动跳过
//veryLongTest中假设后剩下的语句,去执行下一个JUnit测试用例,否则执行假设后接下来的语句
assumeThat( System.getProperty( "DEV" ), nullValue() );
System.out.println("running a long test");
Thread.sleep( 90 * 1000 );
}
开发人员可以使用 assumeThat
并配合 hamcrest 的匹配符
Matcher,对即将被传入到单元测试用例函数中的 runtime 变量值做精确的假设,如果假设不正确(即当前 runtime
变量的取值不满足所假设的条件),则不会将该变量传给该测试用例中假设后面的语句,即程序会从该 assumeThat
所在的 @Test
测试函数中直接自动跳出(test automatically quietly passes,values that violate assumptions are quietly ignored),去执行下一个 @Test
函数,使得本来会中断的测试现在不会中断。
使用假设机制必须得注意以下几点:
- 由于 JUnit 4.4 引用了 Hamcrest 匹配符库,所以使用
assumeThat
就可以编写所有的假设语句。但是为了方便使用,JUnit 4.4 除assumeThat
之外,还提供了assumeTrue
,assumeNotNull
和assumeNoException
语句。 - 要使用
assume*
假设语句,必须得import static org.junit.Assume.*;
。 - 如果引用了第三方 hamcrest 的匹配符库,必须得
import static org.hamcrest.Matchers.*;
,如果引用 JUnit 4.4 自带的匹配符库,需要import static org.hamcrest.CoreMatchers.*;
。
|
当今软件开发中,测试驱动开发(TDD — Test-driven development)越发流行。为什么 TDD 会如此流行呢?因为它确实拥有很多优点,它允许开发人员通过简单的例子来指定和表明他们代码的行为意图。
TDD 的优点:
- 使得开发人员对即将编写的软件任务具有更清晰的认识,使得他们在思考如何编写代码之前先仔细思考如何设计软件。
- 对测试开发人员所实现的代码提供了快速和自动化的支持。
- 提供了一系列可以重用的回归测试用例(regression test case),这些测试用例可以用来检测未来添加的新代码是否改变了以前系统定义的行为(测试代码兼容性)。
然而,TDD 也同样具有一定的局限性。对于开发人员来说,只用一些具体有限的简单例子来表达程序的行为往往远远不够。有很多代码行为可以很容易而且精确的用语言来描 述,却很难用一些简单的例子来表达清楚,因为他们需要大量的甚至无限的具体例子才可以达到被描述清楚的目的,而且有时有限的例子根本不能覆盖所有的代码行 为。
以下列出的代码行为反映了 TDD 的局限性:
- 将十进制整数转换成罗马数字,然后再将其转换回十进制数,并保持原有的数值。(需要大量的测试用例,有限的测试数据可能测不出所实现的代码的错误)。
- 对一个对象进行操作,希望结果仍然等于原来的对象。(需要考虑各种各样类型的对象)
- 在任何一个货币的 collection 中添加一个对象 dollar,需要产生出另外一个新的与以前不同的 collection 。(需要考虑所有的 collection 类型的对象)。
理论(Theory)的出现就是为了解决 TDD 这个问题。 TDD 为组织规划开发流程提供了一个方法,先用一些具体的例子(测试用例 test case)来描述系统代码的行为,然后再将这些行为用代码语句进行概括性的总的陈述(代码实现 implementation)。而 Theory 就是对传统的 TDD 进行一个延伸和扩展,它使得开发人员从开始的定义测试用例的阶段就可以通过参数集(理论上是无限个参数)对代码行为进行概括性的总的陈述,我们叫这些陈述 为理论。理论就是对那些需要无穷个测试用例才能正确描述的代码行为的概括性陈述。结合理论(Theory)和测试一起,可以轻松的描述代码的行为并发现 BUG 。开发人员都知道他们代码所想要实现的概括性的总的目的,理论使得他们只需要在一个地方就可以快速的指定这些目的,而不要将这些目的翻译成大量的独立的测 试用例。
- 优点 1 :理论(Theory)使得开发完全抽象的接口(Interface)更加容易。
- 优点 2 :理论仍然可以重用以前的测试用例,因为以前的许多传统的具体的测试用例仍然可以被轻松的改写成理论(Theory)测试实例。
- 优点 3 :理论(Theory)可以测试出一些原本测试用例没测出来的 bugs 。
- 优点 4 :理论允许配合自动化测试工具进行使用,自动化工具通过大量的数据点来测试一个理论,从而可以放大增强理论的效果。利用自动化工具来分析代码,找出可以证明理论错误的值。
下面通过一个简单的例子来逐步介绍理论的优点。
比如设计一个专门用来货币计算的计算器,首先需要给代码行为编写测试用例(这里以英镑 Pound 的乘法为例),如清单 9 所示:
|
这时很自然的就会想到一个测试用例可能不够,需要再多一个,如清单 10 所示:
|
但是此时您可能又会发现这两个测试用例还是很有限,您所希望的是测试所有的整数,而不只是 2,3 和 5,这些只是您所想要的测试的数据的子集,两个测试用例并不能完全与您所想要测试的代码的行为相等价,您需要更多的测试用例,此时就会发现需要很多的额外 工作来编写这些测试用例,更可怕的是,您会发现您需要测试用例的并不只是简单的几个,可能是成千上万个甚至无穷个测试用例才能满足等价您的代码行为的目 的。
很自然的,您会想到用清单 11 所示的代码来表达您的测试思想。
|
利用清单 11 的 multiplyAnyAmountByInteger
方法,可以轻松将测试用例改写成如清单 12 所示:
|
如清单 12 所示,以后若想增加测试用例,只要不停调用 multiplyAnyAmountByInteger 方法并赋予参数值即可。
方法 multiplyAnyAmountByInteger
就是一个理论的简单例子,理论就是一个带有参数的方法,其行为就是对任何参数都是正常的返回,不会抛出断言错误和其它异常。理论就是对一组数据进行概括性
的陈述,就像一个科学理论一样,如果没有对所有可能出现的情况都进行实验,是不能证明该理论是正确的,但是只要有一种错误情况出现,该理论就不成立。相反
地,一个测试就是对一个单独数据的单独陈述,就像是一个科学理论的实验一样。
在 JUnit 4.4 的理论机制中,每个测试方法不再是由注释 @Test
指定的无参测试函数,而是由注释 @Theory
指定的带参数的测试函数,这些参数来自一个数据集(data sets),数据集通过注释 @DataPoint
指定。
JUnit 4.4 会自动将数据集中定义的数据类型和理论测试方法定义的参数类型进行比较,如果类型相同,会将数据集中的数据通过参数一一传入到测试方法中。数据集中的每一 个数据都会被传入到每个相同类型的参数中。这时有人会问了,如果参数有多个,而且类型都和数据集中定义的数据相同,怎么办?答案是,JUnit 4.4 会将这些数据集中的数据进行一一配对组合(所有的组合情况都会被考虑到),然后将这些数据组合统统通过参数,一一传入到理论的测试方法中,但是用户可以通 过假设机制(assumption)在断言函数(assertion)执行这些参数之前,对这些通过参数传进来的数据集中的数据进行限制和过滤,达到有目 的地部分地将自己想要的参数传给断言函数(assertion)来测试。只有满足所有假设的数据才会执行接下来的测试用例,任何一个假设不满足的数据,都 会自动跳过该理论测试函数(假设 assumption 不满足的数据会被忽略,不再执行接下来的断言测试),如果所有的假设都满足,测试用例断言函数不通过才代表着该理论测试不通过。
|
本文通过详细深入的理论介绍和简单易懂的实例全面剖析了 JUnit 4.4 的三个新特性:
- 提供了新的断言语法(assertion syntax)——
assertThat
- 提供了假设机制(assumptions)
- 提供了理论机制(Theories)
相信读者看完后一定会对 JUnit 4.4 有着非常深入的了解并可以轻松将其运用到自己的开发工程中。
学习
-
获取 Hamcrest
详细信息。
-
教程 如何编写自定义的匹配符
。
- “JUnit 4 抢先看
”(developerWorks,2005 年 10 月):本文以 JUnit 4 为例,详细介绍了如何在自己的工作中使用这个新框架。
- “追求代码质量: JUnit 4 与 TestNG 的对比
”
(developerWorks,2006 年 9 月):JUnit 4 具有基于注释的新框架,它包含了 TestNG
一些最优异的特性。但这是否意味着 JUnit 4 已经淘汰了 TestNG?Andrew Glover 探讨了这两种框架各自的独特之处,并阐述了
TestNG 独有的三种高级测试特性。
- “深入探索 JUnit 4
”(developerWorks,2007 年 3 月):本教程介绍了如何充分利用由注释实现的 JUnit 4 新功能,包括参数测试、异常测试及计时测试。
- “单元测试利器 JUnit 4
”(developerWorks,2007 年 2 月):本文主要介绍了如何使用 JUnit 4 提供的各种功能开展有效的单元测试,并通过一个实例演示了如何使用 Ant 执行自动化的单元测试。
-
通过 popper 工程
了解更多的理论(Theory)知识。
获得产品和技术
从 JUnit 官方网站 下载 JUnit 4.4。
转自【http://www.ibm.com/developerworks/cn/java/j-lo-junit44/】