- 浏览: 546490 次
- 性别:
- 来自: 杭州
文章分类
最新评论
-
wa114d:
楼主工作几年了,好厉害
一个面试官对面试问题的分析 -
wobuxiaole:
Good,非常好
30岁前男人需要完成的事 -
小逗逗:
Good,非常好
30岁前男人需要完成的事 -
invincibleLiu:
好帖,要顶!(别投我隐藏啊,这是对BBS最原始一种支持)
Java:synchronized修饰符在静态方法与非静态方法上的区别 -
fayedShih:
第三题,不知道对不对
import java.util.con ...
企业牛逼面试题目 高手进来讨论答题
级别: 初级
丁 钦浩 (dingqinh@cn.ibm.com), 软件开发工程师, IBM
2008 年 9 月 24 日
本文通过理论分析和详细例子向读者阐述 JUnit 4.4 所带来的最新特性,读者通过本文的学习,可以轻松掌握使用 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 的使用例子:
清单 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" ) );
}
理论机制(Theory)
为什么要引用理论机制(Theory)
当今软件开发中,测试驱动开发(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 所示:
清单 9 英镑 Pound 乘法的一个测试用例
@Test
public void multiplyPoundsByInteger() {
assertEquals( 10, new Pound(5).times(2).getAmount() );
}
这时很自然的就会想到一个测试用例可能不够,需要再多一个,如清单 10 所示:
清单 10 英镑 Pound 乘法的两个测试用例
@Test
public void multiplyPoundsByInteger () {
assertEquals( 10, new Pound(5).times(2).getAmount() );
assertEquals( 15, new Pound(5).times(3).getAmount() );
}
但是此时您可能又会发现这两个测试用例还是很有限,您所希望的是测试所有的整数,而不只是 2,3 和 5,这些只是您所想要的测试的数据的子集,两个测试用例并不能完全与您所想要测试的代码的行为相等价,您需要更多的测试用例,此时就会发现需要很多的额外工作来编写这些测试用例,更可怕的是,您会发现您需要测试用例的并不只是简单的几个,可能是成千上万个甚至无穷个测试用例才能满足等价您的代码行为的目的。
很自然的,您会想到用清单 11 所示的代码来表达您的测试思想。
清单 11 使用变量辅助编写测试用例
//利用变量来代替具体数据表达测试思想
public void multiplyAnyAmountByInteger(int amount, int multiplier) {
assertEquals( amount * multiplier,
new Pound( amount ).times( multiplier ).getAmount() );
}
利用清单 11 的 multiplyAnyAmountByInteger 方法,可以轻松将测试用例改写成如清单 12 所示:
清单 12 改写的英镑 Pound 乘法的测试用例
@Test
public void multiplyPoundsByInteger () {
multiplyAnyAmountByInteger(5, 2);
multiplyAnyAmountByInteger(5, 3);
}
如清单 12 所示,以后若想增加测试用例,只要不停调用 multiplyAnyAmountByInteger 方法并赋予参数值即可。
方法 multiplyAnyAmountByInteger 就是一个理论的简单例子,理论就是一个带有参数的方法,其行为就是对任何参数都是正常的返回,不会抛出断言错误和其它异常。理论就是对一组数据进行概括性的陈述,就像一个科学理论一样,如果没有对所有可能出现的情况都进行实验,是不能证明该理论是正确的,但是只要有一种错误情况出现,该理论就不成立。相反地,一个测试就是对一个单独数据的单独陈述,就像是一个科学理论的实验一样。
如何使用理论机制
在 JUnit 4.4 的理论机制中,每个测试方法不再是由注释 @Test 指定的无参测试函数,而是由注释 @Theory 指定的带参数的测试函数,这些参数来自一个数据集(data sets),数据集通过注释 @DataPoint 指定。
JUnit 4.4 会自动将数据集中定义的数据类型和理论测试方法定义的参数类型进行比较,如果类型相同,会将数据集中的数据通过参数一一传入到测试方法中。数据集中的每一个数据都会被传入到每个相同类型的参数中。这时有人会问了,如果参数有多个,而且类型都和数据集中定义的数据相同,怎么办?答案是,JUnit 4.4 会将这些数据集中的数据进行一一配对组合(所有的组合情况都会被考虑到),然后将这些数据组合统统通过参数,一一传入到理论的测试方法中,但是用户可以通过假设机制(assumption)在断言函数(assertion)执行这些参数之前,对这些通过参数传进来的数据集中的数据进行限制和过滤,达到有目的地部分地将自己想要的参数传给断言函数(assertion)来测试。只有满足所有假设的数据才会执行接下来的测试用例,任何一个假设不满足的数据,都会自动跳过该理论测试函数(假设 assumption 不满足的数据会被忽略,不再执行接下来的断言测试),如果所有的假设都满足,测试用例断言函数不通过才代表着该理论测试不通过。
清单 13 理论机制举例
import static org.hamcrest.Matchers.*; //指定接下来要使用的Matcher匹配符
import static org.junit.Assume.*; //指定需要使用假设assume*来辅助理论Theory
import static org.junit.Assert.*; //指定需要使用断言assert*来判断测试是否通过
import org.junit.experimental.theories.DataPoint; //需要使用注释@DataPoint来指定数据集
import org.junit.experimental.theories.Theories; //接下来@RunWith要指定Theories.class
import org.junit.experimental.theories.Theory; //注释@Theory指定理论的测试函数
import org.junit.runner.RunWith; //需要使用@RunWith指定接下来运行测试的类
import org.junit.Test;
//注意:必须得使用@RunWith指定Theories.class
@RunWith(Theories.class)
public class TheoryTest {
//利用注释@DataPoint来指定一组数据集,这些数据集中的数据用来证明或反驳接下来定义的Theory理论,
//testNames1和testNames2这两个理论Theory测试函数的参数都是String,所以Junit4.4会将这5个
//@DataPoint定义的String进行两两组合,统统一一传入到testNames1和testNames2中,所以参数名year
//和name是不起任何作用的,"2007"同样有机会会传给参数name,"Works"也同样有机会传给参数year
@DataPoint public static String YEAR_2007 = "2007";
@DataPoint public static String YEAR_2008 = "2008";
@DataPoint public static String NAME1 = "developer";
@DataPoint public static String NAME2 = "Works";
@DataPoint public static String NAME3 = "developerWorks";
//注意:使用@Theory来指定测试函数,而不是@Test
@Theory
public void testNames1( String year, String name ) {
assumeThat( year, is("2007") ); //year必须是"2007",否则跳过该测试函数
System.out.println( year + "-" + name );
assertThat( year, is("2007") ); //这里的断言语句没有实际意义,这里举此例只是为了不中断测试
}
//注意:使用@Theory来指定测试函数,而不是@Test
@Theory
public void testNames2( String year, String name ) {
assumeThat(year, is("2007")); //year必须是"2007",否则跳过该测试函数
//name必须既不是"2007"也不是"2008",否则跳过该测试函数
assumeThat(name, allOf( not(is("2007")), not(is("2008"))));
System.out.println( year + "-" + name );
assertThat( year, is("2007") ); //这里的断言语句没有实际意义,这里举此例只是为了不中断测试
}
结果输出:
第一个Theory打印出:
2007-2007
2007-2008
2007-developer
2007-Works
2007-developerWorks
第二个Theory打印出:
2007-developer
2007-Works
2007-developerWorks
结束语
本文通过详细深入的理论介绍和简单易懂的实例全面剖析了 JUnit 4.4 的三个新特性:
提供了新的断言语法(assertion syntax)——assertThat
提供了假设机制(assumptions)
提供了理论机制(Theories)
相信读者看完后一定会对 JUnit 4.4 有着非常深入的了解并可以轻松将其运用到自己的开发工程中。
丁 钦浩 (dingqinh@cn.ibm.com), 软件开发工程师, IBM
2008 年 9 月 24 日
本文通过理论分析和详细例子向读者阐述 JUnit 4.4 所带来的最新特性,读者通过本文的学习,可以轻松掌握使用 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 的使用例子:
清单 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" ) );
}
理论机制(Theory)
为什么要引用理论机制(Theory)
当今软件开发中,测试驱动开发(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 所示:
清单 9 英镑 Pound 乘法的一个测试用例
@Test
public void multiplyPoundsByInteger() {
assertEquals( 10, new Pound(5).times(2).getAmount() );
}
这时很自然的就会想到一个测试用例可能不够,需要再多一个,如清单 10 所示:
清单 10 英镑 Pound 乘法的两个测试用例
@Test
public void multiplyPoundsByInteger () {
assertEquals( 10, new Pound(5).times(2).getAmount() );
assertEquals( 15, new Pound(5).times(3).getAmount() );
}
但是此时您可能又会发现这两个测试用例还是很有限,您所希望的是测试所有的整数,而不只是 2,3 和 5,这些只是您所想要的测试的数据的子集,两个测试用例并不能完全与您所想要测试的代码的行为相等价,您需要更多的测试用例,此时就会发现需要很多的额外工作来编写这些测试用例,更可怕的是,您会发现您需要测试用例的并不只是简单的几个,可能是成千上万个甚至无穷个测试用例才能满足等价您的代码行为的目的。
很自然的,您会想到用清单 11 所示的代码来表达您的测试思想。
清单 11 使用变量辅助编写测试用例
//利用变量来代替具体数据表达测试思想
public void multiplyAnyAmountByInteger(int amount, int multiplier) {
assertEquals( amount * multiplier,
new Pound( amount ).times( multiplier ).getAmount() );
}
利用清单 11 的 multiplyAnyAmountByInteger 方法,可以轻松将测试用例改写成如清单 12 所示:
清单 12 改写的英镑 Pound 乘法的测试用例
@Test
public void multiplyPoundsByInteger () {
multiplyAnyAmountByInteger(5, 2);
multiplyAnyAmountByInteger(5, 3);
}
如清单 12 所示,以后若想增加测试用例,只要不停调用 multiplyAnyAmountByInteger 方法并赋予参数值即可。
方法 multiplyAnyAmountByInteger 就是一个理论的简单例子,理论就是一个带有参数的方法,其行为就是对任何参数都是正常的返回,不会抛出断言错误和其它异常。理论就是对一组数据进行概括性的陈述,就像一个科学理论一样,如果没有对所有可能出现的情况都进行实验,是不能证明该理论是正确的,但是只要有一种错误情况出现,该理论就不成立。相反地,一个测试就是对一个单独数据的单独陈述,就像是一个科学理论的实验一样。
如何使用理论机制
在 JUnit 4.4 的理论机制中,每个测试方法不再是由注释 @Test 指定的无参测试函数,而是由注释 @Theory 指定的带参数的测试函数,这些参数来自一个数据集(data sets),数据集通过注释 @DataPoint 指定。
JUnit 4.4 会自动将数据集中定义的数据类型和理论测试方法定义的参数类型进行比较,如果类型相同,会将数据集中的数据通过参数一一传入到测试方法中。数据集中的每一个数据都会被传入到每个相同类型的参数中。这时有人会问了,如果参数有多个,而且类型都和数据集中定义的数据相同,怎么办?答案是,JUnit 4.4 会将这些数据集中的数据进行一一配对组合(所有的组合情况都会被考虑到),然后将这些数据组合统统通过参数,一一传入到理论的测试方法中,但是用户可以通过假设机制(assumption)在断言函数(assertion)执行这些参数之前,对这些通过参数传进来的数据集中的数据进行限制和过滤,达到有目的地部分地将自己想要的参数传给断言函数(assertion)来测试。只有满足所有假设的数据才会执行接下来的测试用例,任何一个假设不满足的数据,都会自动跳过该理论测试函数(假设 assumption 不满足的数据会被忽略,不再执行接下来的断言测试),如果所有的假设都满足,测试用例断言函数不通过才代表着该理论测试不通过。
清单 13 理论机制举例
import static org.hamcrest.Matchers.*; //指定接下来要使用的Matcher匹配符
import static org.junit.Assume.*; //指定需要使用假设assume*来辅助理论Theory
import static org.junit.Assert.*; //指定需要使用断言assert*来判断测试是否通过
import org.junit.experimental.theories.DataPoint; //需要使用注释@DataPoint来指定数据集
import org.junit.experimental.theories.Theories; //接下来@RunWith要指定Theories.class
import org.junit.experimental.theories.Theory; //注释@Theory指定理论的测试函数
import org.junit.runner.RunWith; //需要使用@RunWith指定接下来运行测试的类
import org.junit.Test;
//注意:必须得使用@RunWith指定Theories.class
@RunWith(Theories.class)
public class TheoryTest {
//利用注释@DataPoint来指定一组数据集,这些数据集中的数据用来证明或反驳接下来定义的Theory理论,
//testNames1和testNames2这两个理论Theory测试函数的参数都是String,所以Junit4.4会将这5个
//@DataPoint定义的String进行两两组合,统统一一传入到testNames1和testNames2中,所以参数名year
//和name是不起任何作用的,"2007"同样有机会会传给参数name,"Works"也同样有机会传给参数year
@DataPoint public static String YEAR_2007 = "2007";
@DataPoint public static String YEAR_2008 = "2008";
@DataPoint public static String NAME1 = "developer";
@DataPoint public static String NAME2 = "Works";
@DataPoint public static String NAME3 = "developerWorks";
//注意:使用@Theory来指定测试函数,而不是@Test
@Theory
public void testNames1( String year, String name ) {
assumeThat( year, is("2007") ); //year必须是"2007",否则跳过该测试函数
System.out.println( year + "-" + name );
assertThat( year, is("2007") ); //这里的断言语句没有实际意义,这里举此例只是为了不中断测试
}
//注意:使用@Theory来指定测试函数,而不是@Test
@Theory
public void testNames2( String year, String name ) {
assumeThat(year, is("2007")); //year必须是"2007",否则跳过该测试函数
//name必须既不是"2007"也不是"2008",否则跳过该测试函数
assumeThat(name, allOf( not(is("2007")), not(is("2008"))));
System.out.println( year + "-" + name );
assertThat( year, is("2007") ); //这里的断言语句没有实际意义,这里举此例只是为了不中断测试
}
结果输出:
第一个Theory打印出:
2007-2007
2007-2008
2007-developer
2007-Works
2007-developerWorks
第二个Theory打印出:
2007-developer
2007-Works
2007-developerWorks
结束语
本文通过详细深入的理论介绍和简单易懂的实例全面剖析了 JUnit 4.4 的三个新特性:
提供了新的断言语法(assertion syntax)——assertThat
提供了假设机制(assumptions)
提供了理论机制(Theories)
相信读者看完后一定会对 JUnit 4.4 有着非常深入的了解并可以轻松将其运用到自己的开发工程中。
发表评论
-
性能分析的几个必读的帖子:
2012-09-29 13:04 1130几个必读的帖子: JVM调优的"标准参数&quo ... -
性能分析的几个必读的帖子
2012-09-29 13:03 0JVM调优的"标准参数"的各种陷阱:htt ... -
使用jmap和MAT定位内存泄漏OOM
2012-09-28 16:41 2222注意分析 heap profile 文件 1 运行应用程序 ... -
Java:synchronized修饰符在静态方法与非静态方法上的区别
2011-09-22 22:18 2477synchronized在静态方法上表示调用前要获得类的锁,而 ... -
编码之前,请想一想 zz
2011-09-22 13:40 1067第 I 条 (a) 这个功能是干什么的?它跟哪些功能 ... -
代码评审怎么做,做到什么程度合适?
2011-03-04 10:03 935来自:http://www.iteye.com/t ... -
写给我的团队成员(一)——什么是BUG?
2011-03-04 09:42 1135来自:http://www.iteye.c ... -
服务器端编程的十大性能问题
2010-11-02 18:37 912今年5月底,瑞士计算机 ... -
Util
2010-02-23 11:18 1170/** * Write the context ... -
java 性能优化
2009-07-17 13:19 2237一、避免在循环条件中使用复杂表达式 在不做编译优化的情况下, ... -
java代码优化
2008-11-06 13:10 1333可供程序利用的资源(内存、CPU时间、网络带宽等)是有限的,优 ... -
junit4
2008-11-04 13:43 1216junit4比junit3.8有了较大的改进,主要体现在 1. ... -
Effective Java
2008-10-23 14:35 12621. 创建和销毁对象 1) ... -
JAVA高性能编程
2008-10-21 10:34 2535如何java高效編程,我在这里抛砖引玉了。希望各位大牛们能把自 ...
相关推荐
JUnit 4.4兼容之前的版本,这意味着大部分旧的JUnit测试用例可以在新版本中无缝运行。 通过这些改进,JUnit 4.4提升了测试的灵活性、可读性和可维护性,使得开发者能够更好地编写和维护高质量的代码。不过,需要...
junit4.4 的jar包 不包含文档,谢谢
"junit4.4+junit4.5+开源码打包下载"这个标题和描述指向的是JUnit的两个版本——4.4和4.5,以及它们的源代码。下面我们将深入探讨JUnit以及这两个版本的关键特性。 **JUnit 4.4** 是JUnit系列的一个重要里程碑,...
标题"junit4.4 下载"指的是获取JUnit 4.4版本,这是该工具的一个特定发行版,发布于2007年8月14日。 JUnit 4.4版本包含了多项改进和新特性,使得测试编写更加灵活和高效。以下是一些关键知识点: 1. **注解...
本文将详细探讨JUnit 4.4的核心特性、用法以及如何在实际开发中有效地利用它进行单元测试。 首先,我们要明确单元测试的重要性。单元测试是对软件中的最小可测试单元进行检查,如函数、方法或类。通过单元测试,...
这个版本的JUnit引入了许多新特性,如注解(Annotations)的支持,使得测试用例的编写更加简洁和直观。 1. **注解支持**:在JUnit 4.4中,我们可以使用`@Test`注解来标记测试方法,`@Before`和`@After`注解来定义在...
JUnit 4是一个重大改进,引入了许多新特性和增强,使得测试更加灵活和强大: 1. **注解的广泛使用**:除了`@Test`外,还引入了`@Before`和`@After`注解,分别用于在每个测试方法前和后执行代码。另外,`@...
在本文中,我们将深入探讨JUnit 4.4的主要特性和使用方法。 首先,JUnit 4.4引入了注解(Annotation)的概念,这是其相对于早期版本的一大改进。注解使得测试类和方法的声明更加简洁,减少了对继承和接口的依赖。...
这是JUnit4.4官方的帮助文件。我把它做成chm格式发上来,希望对大家有用
JUnit 4.4则是对3.8.1的一个显著升级,引入了以下新特性: 1. **注解驱动的测试**:如前所述,JUnit 4引入了注解,使得测试代码更加简洁,减少了元编程的需求。 2. **参数化测试**:`@Parameters`注解配合`@Test`...
标题中的"junit4.4.jar +easymock3.0.jar"提及了两个重要的Java测试库:JUnit 4.4和EasyMock 3.0。这些是用于开发过程中的单元测试的关键工具。 JUnit是Java编程语言中最广泛使用的单元测试框架之一。它允许开发者...
本例介绍了利用了 spring框架进行测试
这是一个基于Spring 3.2.5、Spring MVC 3.2.5、MyBatis 3.2.2和JUnit 4.4的Java Web项目,搭配MySQL数据库的开发环境配置。这个压缩包包含了构建一个功能完备的后端服务所需的基础组件。下面将详细介绍这些技术及其...
在实际开发中,理解和熟练掌握Spring Framework 2.5.6的关键特性,以及如何有效地利用JUnit 4.4和Spring Test进行测试,将极大地提升我们的开发效率和代码质量。对于任何使用Spring的项目来说,这都是一个不可或缺的...
4. **junit4.4.jar**: JUnit是Java编程语言的一个单元测试框架,版本4.4。它是测试驱动开发(TDD)和行为驱动开发(BDD)的重要工具,帮助开发者编写可重复运行的测试用例,确保代码的质量和稳定性。JUnit 4.4引入了注解...
JUnit is a simple framework to write repeatable tests. It is an instance of the xUnit architecture for unit testing frameworks.
在“junit框架文件包”中,提供的主要文件是junit4.4,这代表了JUnit框架的一个特定版本。JUnit 4.4是一个重要的更新,引入了许多改进和新特性,包括: 1. 注解(Annotations):JUnit 4.4引入了注解机制,使得测试...
JUnit 4.4 主要提供了以下三个大方面的新特性来更好的抓住编程人员的代码意图: 提供了新的断言语法(Assertion syntax)——assertThat 提供了假设机制(Assumption) 提供了理论机制(Theory)