探索 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 4.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 的基本语法如下:

清单 1 assertThat 基本语法

assertThat( [value], [matcher statement] );

value 是接下来想要测试的变量值;
matcher statement 是使用 Hamcrest 匹配符来表达的对前面变量所期望的值的声明,如果 value 值与 matcher statement 所表达的期望值相符,则测试成功,否则测试失败。

assertThat 的优点
优点 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 接口,定制自己想要的匹配符。当开发人员发现自己的某些测试代码在不同的测试中重复出现,经常被使用,这时用户就可以自定义匹配符,将这些代码绑定在一个断言语句中,从而可以达到减少重复代码并且更加易读的目的。(具体怎么实现自定义可配置的匹配符,请参阅参考资料)

如何使用 assertThat
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 的使用例子:

//一般匹配符

// allOf匹配符表明如果接下来的所有条件必须都成立测试才通过,相当于“与”(&&)
assertThat( testedNumber, allOf( greaterThan(8), lessThan(16) ) );
// anyOf匹配符表明如果接下来的所有条件只要有一个成立则测试通过,相当于“或”(||)
assertThat( testedNumber, anyOf( greaterThan(16), lessThan(8) ) );
// 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在20.0±0.5范围之内则测试通过
assertThat( testedDouble, closeTo( 20.0, 0.5 ) );
// greaterThan匹配符表明如果所测试的数值testedNumber大于16.0则测试通过
assertThat( testedNumber, greaterThan(16.0) );
// lessThan匹配符表明如果所测试的数值testedNumber小于16.0则测试通过
assertThat( testedNumber, lessThan (16.0) );
// greaterThanOrEqualTo匹配符表明如果所测试的数值testedNumber大于等于16.0则测试通过
assertThat( testedNumber, greaterThanOrEqualTo (16.0) );
// lessThanOrEqualTo匹配符表明如果所测试的数值testedNumber小于等于16.0则测试通过
assertThat( testedNumber, lessThanOrEqualTo (16.0) );

//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(如: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 语句。
假设机制(Assumption)的优点
优点 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 );
}

如何使用 Assumption 假设机制
开发人员可以使用 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.*;。
清单 8 假设机制使用举例

例1:
@Test 
public void filenameIncludesString() {
    //如果文件分隔符不是’/’(forward slash),则不执行assertThat断言测试,直接跳过该测试用例函数
    assumeThat(File.separatorChar, is('/'));
    //判断文件名fileName是否含有字符串"developerWorks"
    assertThat( fileName, containsString( "developerWorks" ) );
}

例2:
@Test 
public void filenameIncludesString() {
    //bugFixed不是JUnit4.4的函数,是开发人员自己工程中定义的函数,表示判断指定的defect是否
    //被修正了,如果被修正,则返回true,否则返回false。这里假设缺陷13356被修正后才进行余下单元测试
    assumeTrue( bugFixed("13356") ); 
    //判断文件名fileName是否含有字符串"developerWorks"
    assertThat( fileName, containsString( "developerWorks" ) );
}

原文链接:https://www.ibm.com/developerworks/cn/java/j-lo-junit44/


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值