文通过理论分析和详细例子向读者阐述 JUnit 所带来的最新特性读者通过本文的学习可以轻松掌握使用 JUnit 的新特性
随着当前 Java 开发的越发成熟Agile 和 TDD 的越发流行自动化测试的呼声也越来越高若想将单元测试变得自动化自然 JUnit 这把利器必不可少这也是 JUnit 自 年诞生以来在 Java 开发业界一直相当流行的原因
JUnit 是针对 Java 语言的一个单元测试框架它被认为是迄今为止所开发的最重要的第三方 Java 库 JUnit 的优点是整个测试过程无需人的参与无需分析和判断最终测试结果是否正确而且可以很容易地一次性运行多个测试 JUnit 的出现促进了测试的盛行它使得 Java 代码更健壮更可靠Bug 比以前更少
JUnit 自从问世以来一直在不停的推出新版本目前最新的版本是 年 月发布的 JUnit 它是继 JUnit 以来最大的发行版提供了很多有用的新特性本文将假设读者已经具有 JUnit 的使用经验
JUnit 概述
JUnit 设计的目的就是有效地抓住编程人员写代码的意图然后快速检查他们的代码是否与他们的意图相匹配 JUnit 发展至今版本不停的翻新但是所有版本都一致致力于解决一个问题那就是如何发现编程人员的代码意图并且如何使得编程人员更加容易地表达他们的代码意图JUnit 也是为了如何能够更好的达到这个目的而出现的
JUnit 主要提供了以下三个大方面的新特性来更好的抓住编程人员的代码意图
提供了新的断言语法(Assertion syntax)——assertThat
提供了假设机制(Assumption)
提供了理论机制(Theory)
新的断言语法(Assertion syntax)—— assertThat
JUnit 学习 JMock引入了 Hamcrest 匹配机制使得程序员在编写单元测试的 assert 语句时可以具有更强的可读性而且也更加灵活
Hamcrest 是一个测试的框架它提供了一套通用的匹配符 Matcher灵活使用这些匹配符定义的规则程序员可以更加精确的表达自己的测试思想指定所想设定的测试条件比如有时候定义的测试数据范围太精确往往是若干个固定的确定值这时会导致测试非常脆弱因为接下来的测试数据只要稍稍有变化就可能导致测试失败(比如 assertEquals( x ) 只能判断 x 是否等于 如果 x 不等于 测试失败)有时候指定的测试数据范围又不够太精确这时有可能会造成某些本该会导致测试不通过的数据仍然会通过接下来的测试这样就会降低测试的价值 Hamcrest 的出现给程序员编写测试用例提供了一套规则和方法使用其可以更加精确的表达程序员所期望的测试的行为(具体 Hamcrest 的使用请参阅 参考资料)
JUnit 结合 Hamcrest 提供了一个全新的断言语法——assertThat程序员可以只使用 assertThat 一个断言语句结合 Hamcrest 提供的匹配符就可以表达全部的测试思想
assertThat 的基本语法如下
清单 assertThat 基本语法
assertThat( [value] [matcher statement] );
value 是接下来想要测试的变量值
matcher statement 是使用 Hamcrest 匹配符来表达的对前面变量所期望的值的声明
如果 value 值与 matcher statement 所表达的期望值相符
则测试成功
否则测试失败
assertThat 的优点
优点 以前 JUnit 提供了很多的 assertion 语句如assertEqualsassertNotSameassertFalseassertTrueassertNotNullassertNull 等现在有了 JUnit 一条 assertThat 即可以替代所有的 assertion 语句这样可以在所有的单元测试中只使用一个断言方法使得编写测试用例变得简单代码风格变得统一测试代码也更容易维护
优点 assertThat 使用了 Hamcrest 的 Matcher 匹配符用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件具有很强的易读性而且使用起来更加灵活如清单 所示
清单使用匹配符 Matcher 和不使用之间的比较
// 想判断某个字符串 s 是否含有子字符串 developer 或 Works 中间的一个
// JUnit 以前的版本assertTrue(sindexOf(developer)>||sindexOf(Works)> );
// JUnit
assertThat(s anyOf(containsString(developer) containsString(Works)));
// 匹配符 anyOf 表示任何一个条件满足则成立类似于逻辑或 || 匹配符 containsString 表示是否含有参数子
// 字符串文章接下来会对匹配符进行具体介绍
优点 assertThat 不再像 assertEquals 那样使用比较难懂的谓宾主语法模式(如assertEquals( x))相反assertThat 使用了类似于主谓宾的易读语法模式(如assertThat(xis()))使得代码更加直观易读
优点 可以将这些 Matcher 匹配符联合起来灵活使用达到更多目的如清单 所示
清单 Matcher 匹配符联合使用
// 联合匹配符not和equalTo表示不等于
assertThat( something not( equalTo( developer ) ) );
// 联合匹配符not和containsString表示不包含子字符串
assertThat( something not( containsString( Works ) ) );
// 联合匹配符anyOf和containsString表示包含任何一个子字符串
assertThat(something anyOf(containsString(developer) containsString(Works)));
优点 错误信息更加易懂可读且具有描述性(descriptive)
JUnit 以前的版本默认出错后不会抛出额外提示信息如
assertTrue( sindexOf(developer) > || sindexOf(Works) > );
如果该断言出错只会抛出无用的错误信息如junitframeworkAssertionFailedErrornull
如果想在出错时想打印出一些有用的提示信息必须得程序员另外手动写如
assertTrue( Expected a string containing developer or Works
sindexOf(developer) > || sindexOf(Works) > );
非常的不方便而且需要额外代码
JUnit 会默认自动提供一些可读的描述信息如清单 所示
清单 JUnit 默认提供一些可读的描述性错误信息
String s = hello world!;
assertThat( s anyOf( containsString(developer) containsString(Works) ) );
// 如果出错后系统会自动抛出以下提示信息
javalangAssertionError:
Expected: (a string containing developer or a string containing Works)
got: hello world!
优点 开发人员可以通过实现 Matcher 接口定制自己想要的匹配符当开发人员发现自己的某些测试代码在不同的测试中重复出现经常被使用这时用户就可以自定义匹配符将这些代码绑定在一个断言语句中从而可以达到减少重复代码并且更加易读的目的
如何使用 assertThat
JUnit 自带了一些 Hamcrest 的匹配符 Matcher但是只有有限的几个在类 orghamcrestCoreMatchers 中定义要想使用他们必须导入包 orghamcrestCoreMatchers*
如果想使用一些其他更多的匹配符 Matcher可以从 Hamcrest 网页下载 hamcrestlibraryjar 和 hamcrestcorejar并将其加入到工程库中所有的匹配符都在类 orghamcrestMatchers 中定义要想使用必须得在代码中 import static orghamcrestMatchers*如果使用外部的匹配符最好就不要再使用 JUnit 自带的匹配符了因为这样容易导致匹配符 Matcher 重复定义编译可能会出错(ambiguous for the type) JUnit 允许使用 Hamcrest 来使用更多的匹配符这还是 JUnit 第一次允许在自己的工程中使用第三方类
注意
assertThat 仍然是断言语句所以要想使用必须还得 import static orgjunitAssert*
虽然 assertThat 可以代替以前所有的断言语句但是以前的所有 assert 语句仍然可以继续使用清单 列举了大部分 assertThat 的使用例子
清单 assertThat 使用举例
//一般匹配符
// allOf匹配符表明如果接下来的所有条件必须都成立测试才通过相当于与(&&)
assertThat( testedNumber allOf( greaterThan() lessThan() ) );
// anyOf匹配符表明如果接下来的所有条件只要有一个成立则测试通过相当于或(||)
assertThat( testedNumber anyOf( greaterThan() lessThan() ) );
// anything匹配符表明无论什么条件永远为true
assertThat( testedNumber anything() );
// is匹配符表明如果前面待测的object等于后面给出的object则测试通过
assertThat( testedString is( developerWorks ) );
// not匹配符和is匹配符正好相反表明如果前面待测的object不等于后面给出的object则测试通过
assertThat( testedString not( developerWorks ) );
//字符串相关匹配符
// containsString匹配符表明如果测试的字符串testedString包含子字符串developerWorks则测试通过
assertThat( testedString containsString( developerWorks ) );
// endsWith匹配符表明如果测试的字符串testedString以子字符串developerWorks结尾则测试通过
assertThat( testedString endsWith( developerWorks ) );
// startsWith匹配符表明如果测试的字符串testedString以子字符串developerWorks开始则测试通过
assertThat( testedString startsWith( developerWorks ) );
// equalTo匹配符表明如果测试的testedValue等于expectedValue则测试通过equalTo可以测试数值之间字
//符串之间和对象之间是否相等相当于Object的equals方法
assertThat( testedValue equalTo( expectedValue ) );
// equalToIgnoringCase匹配符表明如果测试的字符串testedString在忽略大小写的情况下等于
//developerWorks则测试通过
assertThat( testedString equalToIgnoringCase( developerWorks ) );
// equalToIgnoringWhiteSpace匹配符表明如果测试的字符串testedString在忽略头尾的任意个空格的情况下等
//于developerWorks则测试通过注意字符串中的空格不能被忽略
assertThat( testedString equalToIgnoringWhiteSpace( developerWorks ) );
//数值相关匹配符
// closeTo匹配符表明如果所测试的浮点型数testedDouble在±范围之内则测试通过
assertThat( testedDouble closeTo( ) );
// greaterThan匹配符表明如果所测试的数值testedNumber大于则测试通过
assertThat( testedNumber greaterThan() );
// lessThan匹配符表明如果所测试的数值testedNumber小于则测试通过
assertThat( testedNumber lessThan () );
// greaterThanOrEqualTo匹配符表明如果所测试的数值testedNumber大于等于则测试通过
assertThat( testedNumber greaterThanOrEqualTo () );
// lessThanOrEqualTo匹配符表明如果所测试的数值testedNumber小于等于则测试通过
assertThat( testedNumber lessThanOrEqualTo () );
//collection相关匹配符
// hasEntry匹配符表明如果测试的Map对象mapObject含有一个键值为key对应元素值为value的Entry项则
//测试通过
assertThat( mapObject hasEntry( key value ) );
// hasItem匹配符表明如果测试的迭代对象iterableObject含有元素element项则测试通过
assertThat( iterableObject hasItem ( element ) );
// hasKey匹配符表明如果测试的Map对象mapObject含有键值key则测试通过
assertThat( mapObject hasKey ( key ) );
// hasValue匹配符表明如果测试的Map对象mapObject含有元素值value则测试通过
assertThat( mapObject hasValue ( key ) );
假设机制(Assumption)
理想情况下写测试用例的开发人员可以明确的知道所有导致他们所写的测试用例不通过的地方但是有的时候这些导致测试用例不通过的地方并不是很容易的被发现可能隐藏得很深从而导致开发人员在写测试用例时很难预测到这些因素而且往往这些因素并不是开发人员当初设计测试用例时真正目的他们的测试点是希望测试出被测代码中别的出错地方
比如一个测试用例运行的 locale(如LocaleUS)与之前开发人员设计该测试用例时所设想的不同(如LocaleUK)这样会导致测试不通过但是这可能并不是开发人员之前设计测试用例时所设想的测试出来的有用的失败结果(测试点并不是此比如测试的真正目的是想判断函数的返回值是否为 true返回 false 则测试失败)这时开发人员可以通过编写一些额外的代码来消除这些影响(比如将 locale 作为参数传入到测试用例中每次运行测试用例时明确指定 locale)但是花费时间和精力来编写这些不是测试用例根本目的的额外代码其实是种浪费这时就可以使用 Assumption 假设机制来轻松达到额外代码的目的编写该测试用例时首先假设 locale 必须是 LocaleUK如果运行时 locale 是 LocaleUK则继续执行该测试用例函数如果是其它的 locale则跳过该测试用例函数执行该测试用例函数以外的代码这样就不会因为 locale 的问题导致测试出错
JUnit 结合 Hamcrest 库提供了 assumeThat 语句开发人员可以使用其配合匹配符 Matcher 设计所有的假设条件(语法和 assertThat 一样)同样为了方便使用JUnit 还专门提供了 assumeTrueassumeNotNull 和 assumeNoException 语句
假设机制(Assumption)的优点
优点 通过对 runtime 变量进行取值假设从而不会因为一个测试用例的不通过而导致整个测试失败而中断(the test passes)使得测试更加连贯
开发人员编写单元测试时经常会在一个测试中包含若干个测试用例函数这时若是遇到某个测试用例函数不通过整个单元测试就会终止这将导致测试不连贯因为开发人员往往希望一次能运行多个测试用例函数不通过的测试用例函数不要影响到剩下的测试用例函数的运行否则会给 debug 调试带来很大的难度
开发人员编写单元测试时有时是预测不了传入到单元测试方法中的变量值的而且这些值有时并不是开发人员所期望的因为他们会导致测试用例不通过并中断整个测试所以开发人员需要跳过这些导致测试用例函数不通过的异常情况
清单 假设机制优点 举例
//@Test 注释表明接下来的函数是 JUnit 及其以后版本的测试用例函数
@Test
public void testAssumptions() {
//假设进入testAssumptions时变量i的值为如果该假设不满足程序不会执行assumeThat后面的语句
assumeThat( i is() );
//如果之前的假设成立会打印assumption is true!到控制台否则直接调出执行下一个测试用例函数
Systemoutprintln( assumption is true! );
}
优点 利用假设可以控制某个测试用例的运行时间让其在自己期望的时候运行(run at a given time)
清单 假设机制优点 举例
@Test
//测试用例函数veryLongTest()执行需要很长时间所以开发人员不是每次都想运行它可以通过判断是否定义了
//DEV环境变量来选择性地执行该测试用例
public void veryLongTest() throws Exception {
//假设环境变量DEV为空即如果之前通过SystemsetProperty定义过DEV环境变量(不为空)则自动跳过
//veryLongTest中假设后剩下的语句去执行下一个JUnit测试用例否则执行假设后接下来的语句
assumeThat( SystemgetProperty( DEV ) nullValue() );
Systemoutprintln(running a long test);
Threadsleep( * );
}
如何使用 Assumption 假设机制
开发人员可以使用 assumeThat 并配合 hamcrest 的匹配符 Matcher对即将被传入到单元测试用例函数中的 runtime 变量值做精确的假设如果假设不正确(即当前 runtime 变量的取值不满足所假设的条件)则不会将该变量传给该测试用例中假设后面的语句即程序会从该 assumeThat 所在的 @Test 测试函数中直接自动跳出(test automatically quietly passesvalues that violate assumptions are quietly ignored)去执行下一个 @Test 函数使得本来会中断的测试现在不会中断
使用假设机制必须得注意以下几点
由于 JUnit
引用了 Hamcrest 匹配符库
所以使用 assumeThat 就可以编写所有的假设语句
但是为了方便使用
JUnit
除 assumeThat 之外
还提供了 assumeTrue
assumeNotNull 和 assumeNoException 语句
要使用 assume* 假设语句
必须得 import static org
junit
Assume
*;
如果引用了第三方 hamcrest 的匹配符库
必须得 import static org
hamcrest
Matchers
*;
如果引用 JUnit
自带的匹配符库
需要 import static org
hamcrest
CoreMatchers
*;
清单 假设机制使用举例
例
@Test
public void filenameIncludesString() {
//如果文件分隔符不是/(forward slash)则不执行assertThat断言测试直接跳过该测试用例函数
assumeThat(FileseparatorChar is(/));
//判断文件名fileName是否含有字符串developerWorks
assertThat( fileName containsString( developerWorks ) );
}
例
@Test
public void filenameIncludesString() {
//bugFixed不是JUnit的函数是开发人员自己工程中定义的函数表示判断指定的defect是否
//被修正了如果被修正则返回true否则返回false这里假设缺陷被修正后才进行余下单元测试
assumeTrue( bugFixed() );
//判断文件名fileName是否含有字符串developerWorks
assertThat( fileName containsString( developerWorks ) );
}
理论机制(Theory)
为什么要引用理论机制(Theory)
当今软件开发中测试驱动开发(TDD — Testdriven development)越发流行为什么 TDD 会如此流行呢?因为它确实拥有很多优点它允许开发人员通过简单的例子来指定和表明他们代码的行为意图
TDD 的优点
使得开发人员对即将编写的软件任务具有更清晰的认识使得他们在思考如何编写代码之前先仔细思考如何设计软件 对测试开发人员所实现的代码提供了快速和自动化的支持 提供了一系列可以重用的回归测试用例(regression test case)这些测试用例可以用来检测未来添加的新代码是否改变了以前系统定义的行为(测试代码兼容性)
然而TDD 也同样具有一定的局限性对于开发人员来说只用一些具体有限的简单例子来表达程序的行为往往远远不够有很多代码行为可以很容易而且精确的用语言来描述却很难用一些简单的例子来表达清楚因为他们需要大量的甚至无限的具体例子才可以达到被描述清楚的目的而且有时有限的例子根本不能覆盖所有的代码行为
以下列出的代码行为反映了 TDD 的局限性
将十进制整数转换成罗马数字然后再将其转换回十进制数并保持原有的数值(需要大量的测试用例有限的测试数据可能测不出所实现的代码的错误) 对一个对象进行操作希望结果仍然等于原来的对象(需要考虑各种各样类型的对象) 在任何一个货币的 collection 中添加一个对象 dollar需要产生出另外一个新的与以前不同的 collection (需要考虑所有的 collection 类型的对象)
理论(Theory)的出现就是为了解决 TDD 这个问题 TDD 为组织规划开发流程提供了一个方法先用一些具体的例子(测试用例 test case)来描述系统代码的行为然后再将这些行为用代码语句进行概括性的总的陈述(代码实现 implementation)而 Theory 就是对传统的 TDD 进行一个延伸和扩展它使得开发人员从开始的定义测试用例的阶段就可以通过参数集(理论上是无限个参数)对代码行为进行概括性的总的陈述我们叫这些陈述为理论理论就是对那些需要无穷个测试用例才能正确描述的代码行为的概括性陈述结合理论(Theory)和测试一起可以轻松的描述代码的行为并发现 BUG 开发人员都知道他们代码所想要实现的概括性的总的目的理论使得他们只需要在一个地方就可以快速的指定这些目的而不要将这些目的翻译成大量的独立的测试用例
理论机制的优点
优点 理论(Theory)使得开发完全抽象的接口(Interface)更加容易
优点 理论仍然可以重用以前的测试用例因为以前的许多传统的具体的测试用例仍然可以被轻松的改写成理论(Theory)测试实例
优点 理论(Theory)可以测试出一些原本测试用例没测出来的 bugs 优点 理论允许配合自动化测试工具进行使用自动化工具通过大量的数据点来测试一个理论从而可以放大增强理论的效果利用自动化工具来分析代码找出可以证明理论错误的值
下面通过一个简单的例子来逐步介绍理论的优点
比如设计一个专门用来货币计算的计算器首先需要给代码行为编写测试用例(这里以英镑 Pound 的乘法为例)如清单 所示
清单 英镑 Pound 乘法的一个测试用例
@Test
public void multiplyPoundsByInteger() {
assertEquals( new Pound()times()getAmount() );
}
这时很自然的就会想到一个测试用例可能不够需要再多一个如清单 所示
清单 英镑 Pound 乘法的两个测试用例
@Test
public void multiplyPoundsByInteger () {
assertEquals( new Pound()times()getAmount() );
assertEquals( new Pound()times()getAmount() );
}
但是此时您可能又会发现这两个测试用例还是很有限您所希望的是测试所有的整数而不只是 和 这些只是您所想要的测试的数据的子集两个测试用例并不能完全与您所想要测试的代码的行为相等价您需要更多的测试用例此时就会发现需要很多的额外工作来编写这些测试用例更可怕的是您会发现您需要测试用例的并不只是简单的几个可能是成千上万个甚至无穷个测试用例才能满足等价您的代码行为的目的
很自然的您会想到用清单 所示的代码来表达您的测试思想
清单 使用变量辅助编写测试用例
//利用变量来代替具体数据表达测试思想
public void multiplyAnyAmountByInteger(int amount int multiplier) {
assertEquals( amount * multiplier
new Pound( amount )times( multiplier )getAmount() );
}
利用清单 的 multiplyAnyAmountByInteger 方法可以轻松将测试用例改写成如清单 所示
清单 改写的英镑 Pound 乘法的测试用例
@Test
public void multiplyPoundsByInteger () {
multiplyAnyAmountByInteger( );
multiplyAnyAmountByInteger( );
}
如清单 所示以后若想增加测试用例只要不停调用 multiplyAnyAmountByInteger 方法并赋予参数值即可
方法 multiplyAnyAmountByInteger 就是一个理论的简单例子理论就是一个带有参数的方法其行为就是对任何参数都是正常的返回不会抛出断言错误和其它异常理论就是对一组数据进行概括性的陈述就像一个科学理论一样如果没有对所有可能出现的情况都进行实验是不能证明该理论是正确的但是只要有一种错误情况出现该理论就不成立相反地一个测试就是对一个单独数据的单独陈述就像是一个科学理论的实验一样
如何使用理论机制
在 JUnit 的理论机制中每个测试方法不再是由注释 @Test 指定的无参测试函数而是由注释 @Theory 指定的带参数的测试函数这些参数来自一个数据集(data sets)数据集通过注释 @DataPoint 指定
JUnit 会自动将数据集中定义的数据类型和理论测试方法定义的参数类型进行比较如果类型相同会将数据集中的数据通过参数一一传入到测试方法中数据集中的每一个数据都会被传入到每个相同类型的参数中这时有人会问了如果参数有多个而且类型都和数据集中定义的数据相同怎么办?答案是JUnit 会将这些数据集中的数据进行一一配对组合(所有的组合情况都会被考虑到)然后将这些数据组合统统通过参数一一传入到理论的测试方法中但是用户可以通过假设机制(assumption)在断言函数(assertion)执行这些参数之前对这些通过参数传进来的数据集中的数据进行限制和过滤达到有目的地部分地将自己想要的参数传给断言函数(assertion)来测试只有满足所有假设的数据才会执行接下来的测试用例任何一个假设不满足的数据都会自动跳过该理论测试函数(假设 assumption 不满足的数据会被忽略不再执行接下来的断言测试)如果所有的假设都满足测试用例断言函数不通过才代表着该理论测试不通过
清单 理论机制举例
import static orghamcrestMatchers*; //指定接下来要使用的Matcher匹配符
import static orgjunitAssume*; //指定需要使用假设assume*来辅助理论Theory
import static orgjunitAssert*; //指定需要使用断言assert*来判断测试是否通过
import orgjunitexperimentaltheoriesDataPoint;//需要使用注释@DataPoint来指定数据集
import orgjunitexperimentaltheoriesTheories; //接下来@RunWith要指定Theoriesclass
import orgjunitexperimentaltheoriesTheory; //注释@Theory指定理论的测试函数
import orgjunitrunnerRunWith; //需要使用@RunWith指定接下来运行测试的类
import orgjunitTest;
//注意必须得使用@RunWith指定Theoriesclass
@RunWith(Theoriesclass)
public class TheoryTest {
//利用注释@DataPoint来指定一组数据集这些数据集中的数据用来证明或反驳接下来定义的Theory理论
//testNames和testNames这两个理论Theory测试函数的参数都是String所以Junit会将这个
//@DataPoint定义的String进行两两组合统统一一传入到testNames和testNames中所以参数名year
//和name是不起任何作用的同样有机会会传给参数nameWorks也同样有机会传给参数year
@DataPoint public static String YEAR_ = ;
@DataPoint public static String YEAR_ = ;
@DataPoint public static String NAME = developer;
@DataPoint public static String NAME = Works;
@DataPoint public static String NAME = developerWorks;
//注意使用@Theory来指定测试函数而不是@Test
@Theory
public void testNames( String year String name ) {
assumeThat( year is() ); //year必须是否则跳过该测试函数
Systemoutprintln( year + + name );
assertThat( year is() ); //这里的断言语句没有实际意义这里举此例只是为了不中断测试
}
//注意使用@Theory来指定测试函数而不是@Test
@Theory
public void testNames( String year String name ) {
assumeThat(year is()); //year必须是否则跳过该测试函数
//name必须既不是也不是否则跳过该测试函数
assumeThat(name allOf( not(is()) not(is())));
Systemoutprintln( year + + name );
assertThat( year is() ); //这里的断言语句没有实际意义这里举此例只是为了不中断测试
}
结果输出:
第一个Theory打印出
developer
Works
developerWorks
第二个Theory打印出
developer
Works
developerWorks
结束语
本文通过详细深入的理论介绍和简单易懂的实例全面剖析了 JUnit 的三个新特性
提供了新的断言语法(assertion syntax)——assertThat提供了假设机制(assumptions)
提供了理论机制(Theories)
相信读者看完后一定会对 JUnit 有着非常深入的了解并可以轻松将其运用到自己的开发工程中