JUnit的核心类主要有三个:TestCase、TestSuite和TestRunner。 TestCase(测试用例)--扩展了JUnit的TestCase类的类。它以testXXX方法的形式包含一个或多个测试,一个test case把具有公共行为的测试归入一组。 TestSuite(测试集合)--一组测试。一个test case是把多个相关测试归入一组的便捷方式。若是你没有为TestCase定义一个test suite,那么JUnit会自动为你提供一个test suite,包含TestCase中所有的测试 TestRunner(测试运行器)--执行test suite的程序。JUnit没有TestRunner接口,但是有一个所有test runner都必须继承的BaseTestRunner。 Assert(断言)--当条件成立时,Assert方法保持沉默,否则就抛出异常。 TestResult(测试结果)--包含了测试中发生的所有错误或失败。 Test接口--可以运行Test并把结果传递给TestResult。 TestListener接口--测试中若产生事件(开始、结束、错误、失败)会通知TestListener。 TestRunner主要使用方式有两种:Console和SwingUI,另外有一种AWT的已经不常用了,而且你还可以通过继承BaseTestRunner来定义你自己的TestRunner。 TestSuite相当于一个容器,可以把几个测试作为一个集合放在一起运行,TestRunner只负责启动TestSuite,而具体执行哪些TestCase是由TestSuite来决定。TestSuite会为每个TestCase需要测试的方法生成一个实例,保持了各个单元测试的独立性。 一个很有趣的现象,TestCase和TestSuite都实现了Test接口,而TestSuite的addTest方法接收的参数类型却是Test接口类型,而不是TestCase类型,这就意味着可以给TestSuite增加TestCase,也可以增加TestSuite,这种灵活性很方便你创建各种Suite或者组合出TestAll类。 TestResult负责收集TestCase的执行结果,TestRunner使用TestResult来报告测试结果,若是TestResult集合中没有TestFailure对象,那么所有的测试就是正确的。 除了TestRunner可以报告测试结果之外,TestListener也可以帮助对象访问TestResult并创建有用的报告,可以有任意数量的TestListener向JUnit框架注册。 TestCase包含两个主要部件:Fixture和单元测试 Fixture--运行一个或多个测试所需的公用资源或数据集合。比如象数据库连接等。TestCase会通过setUP和tearDown方法来自动创建和销毁Fixture,它会在每个测试之前调用setUp,在每个测试之后调用tearDown。把不止一个测试方法放进TestCase的一个重要理由就是可以共享Fixture代码。 单元测试模块主要是包含了Assert类,它提供了一系列assertXXX的方法,很方便我们进行重复的测试,另外TestCase还提供了一些方法。 |
------------------------------------------------------------------------------------------------------------------------------------------
自己定义的TestCase,并使用TestRunner来运行测试,事实上TestRunner并不直接运行 TestCase上的单元方法,而是透过TestSuite,TestSuite可以将数个TestCase在一起,而让每个TestCase保持简单。
------------------------------------------------------------------------------------------------------------------------------------------
了解 JUnit 核心类、接口及生命周期
简介: Junit 从问世至今已有 12 年的历史,期间功能不断完善,用户逐渐扩大,已经成为 Java 软件开发中应用最为广泛的测试框架。本文着重介绍 JUnit 的核心接口、核心类以及 TestCase 的生命周期,以便读者从架构层面掌握这个工具。
1997 年,Erich Gamma 和 Kent Beck 为 Java 语言创建了一个简单但有效的单元测试框架,称作 JUnit。JUnit 很快成为 Java 中开发单元测试的框架标准。世界上无数软件项目使用它。本文将介绍 JUnit 的核心接口,核心类以及 JUnit 的生命周期。
JUnit 核心接口及核心类
了解 JUnit 的生命周期之前,先了解 JUnit 的核心接口和类是有必要的,这对于了解 TestCase 的生命周期有很大的帮助。
Test:是 TestCase、TestSuite 的共同接口。run(TestResult result)
用来运行 Test,并且将结果保存到 TestResult。
TestCase:Test 的接口的抽象实现,是 Abstract 类,所以不能实例化,能被继承。其中一个构造函数 TestCase(String name),根据输入的参数,创建一个测试实例。参数为该类的以 test 开头的方法名,把它添加到 TestSuite 中,指定仅仅运行 TestCase 中的一个方法。
TestSuite:实现 Test 接口。可以组装一个或者多个 TestCase。待测试类中可能包括了对被测类的多个 TestCase,而 TestSuit 可以保存多个 TestCase,负责收集这些测试,这样就可以一个 Suite 就能运行对被测类的多个测试。
TestResult:保存 TestCase 运行中的事件。TestResult 有 List<TestFailure> fFailures
和 List<TestFailure> fErrors
。fFailures 记录 Test 运行中的 AssertionFailedError,而 fErrors 则记录 Exception。Failure 是当期望值和断言不匹配的时候抛出的异常,而 Error 则是不曾预料到的异常,如:ArrayIndexOutOfBoundsException。
TestListener:是个接口,对事件监听,可供 TestRunner 类使用。
ResultPrinter:实现 TestListener 接口。在 TestCase 运行过程中,对所监听的对象的事件以一定格式及时输出,运行完后,对 TestResult 对象进行分析,输出的统计结果。
BaseTestRunner:所有 TestRunner 的超类。
java Junit.swingui.TestRunner:实现 BaseTestRunner,提供图形界面。从 4.0 版本起,就没有再提供这个类。这是 4.0 版本和之前版本的显著变化之一。
java Junit.textui.TestRunner:实现 BaseTestRunner,提供文本界面。下面将以它做为例子讲解 JUnit 生命周期。
回页首
TestCase 实例
了解了前面的几个类,下面将看一个例子:
public class TestShoppingCart extends TestCase {
double unitPrice = 5;
int quantity = 6;
double discount=0.2;
@Before
public void setUp() throws Exception {
System.out.println(" Up ");
}
@After
public void tearDown() throws Exception {
System.out.println(" Down ");
}
public void testPay() {
double total = unitPrice * quantity;
assertEquals(30, total);
}
public void testPayWithDiscount() {
double total = unitPrice * quantity*(1-discount);
assertEquals(24.0, total);
}
}
|
回页首
两种不同参数运行 TestCase
参数 1:
输入:
>java junit.textui.TestRunner TestShoppingCart |
输出:
Up
testPay!
Down
Up
testPayWithDiscount!
Down |
参数 2:
输入:
> java junit.textui.TestRunner -m TestShoppingCart.testPayWithDiscount |
输出:
Up
testPayWithDiscount!
Down |
参数 1:TestCase 名字,该类的所有的以 test 开头的 public 方法都会执行。
参数 2:参数 -m
,仅仅运行该类的该方法。
TestRunner 还提供了其他的参数 -wait
:(最大响应时间),-v
:查看 JUnit 版本号。从输出可以看出,参数一: testPay()
,testPayWithDiscount()
都运行;参数二:仅仅运行参数中的 testPayWithDiscount()
。对比两个输出结果,setUp()
在每个方法运行前运行一次,teardown()
在每个方法运行后执行一次。后面将会详细介绍。
回页首
TestRunner 处理两种不同的参数
TestRunner main()
方法中,生成一个 TestRunner 实例,调用 start(args)
方法。在 start 方法中,JUnit 对输入参数进行处理,首先检查 -m、-v、-wait 等参数,对他们分别进行处理。如果有 -m 参数,将会根据“.”的位置,分割得到 className 和 methodName.
参数一:
首先调用 getTest()
,通过 Java 反射实例化 TestSuite:
Class testClass = Class.forName(suiteClassName).asSubclass(TestCase.class);
new TestSuite(testClass)
|
TestSuite 构造函数中,通过调用 Class.getDeclaredMethods()
,得到这个类的所有 Public 的方法,当然也包括构造函数,test 开头和非 test 开头的 public 方法。对所有方法进行过滤,仅仅保留 public 并且以“test”开头的方法,本例中为 testPay()
和 testPayWithDiscount()
。然后分别调用 TestSuite 的 createTest()
为每个方法生成一个实例:
theClass.getConstructor(String.class).newInstance(new Object[0]);
|
并且都保存在 Vector<Test> fTests
中。
参数二:
与方法一不同的的是,并不通过反射获得相应的方法,因为参数中指定了特定的方法。直接根据输入参数调用 TestSuite 的 createTest()
,通过反射直接生成 TestCase 实例。
回页首
TestCase 实例的运行
生成 TestCase 实例后,两种参数都将调用 TestRunner 的 doRun()
方法。下面将对第二种参数进行详细介绍,介绍一个 TestCase 实例是怎么运行的,并且怎样与 TestResult 和 TestListener 结合。
在 doRun()
方法中,实例化 TestResult result, 为 result 加上 Listener (new ResultPrinter()),用来监听 Test 运行中的事件。然后运行 TestResult.Run(test)
。run()
方法中调用 TestCase 的 runBare()
。runBare()
会把所有的异常都抛出来,result 将接受到所有的异常。runBare()
首先会运行 setup()
,接着运行 runTest()
, 最后 tearDown()
。回头再看前面的 output,就明白了为什么 setup()
和 tearDown()
会在每个方法运行前和后运行,对于参数二,运行了两次。
回页首
TestResult
TestResult 有两个 List,用来记录 Exception 和 Failure。捕获 runBare()
抛出的 Exception,首先判断是否为 AssertionFailedError,是则调用 addFailure()
把,把异常加到 fFailures。否则则并调用 addError()
方法,把异常加到 fErrors 中。
catch (AssertionFailedError e) {
addFailure(test, e);
}
catch (ThreadDeath e) { // don't catch ThreadDeath by accident
throw e;
}
catch (Throwable e) {
ddError(test, e);
}
|
回页首
TestListener
前面提到 result 加上了一个 ResultPrinter,ResultPrinter 会记录运行中的所有 Exception,并且实时地以不同的格式输出。当所有的 Test 都运行完毕后,ResultPrinter 会对 result 进行分析,首先输出运行的时间,接着 printError()
输出 fErrors 的个数,printFailures()
则输出 fFailures 的个数。PrintFooter()
根据 result.wasSuccessful()
,如果成功,则打印 OK 和 test 运行的总次数,如果失败,则打印出 test 总的运行的个数,失败和错误的个数。
参数一的统计输出结果:
Time: 0.016
There was 1 failure:
1) testPay(TestShoppingCart)junit.framework.AssertionFailedError:
expected:<30> but FAILURES!!!
Tests run: 2, Failures: 1, Errors: 0
|
synchronized void print(TestResult result, long runTime) {
printHeader(runTime);
printErrors(result);
printFailures(result);
printFooter(result);
}
|
protected void printFooter(TestResult result) {
if (result.wasSuccessful()) {
getWriter().println();
getWriter().print("OK");
getWriter().println (" (" + result.runCount() + " test"
+ (result.runCount() == 1 ? "": "s") + ")");
} else {
getWriter().println();
getWriter().println("FAILURES!!!");
getWriter().println("Tests run: "+result.runCount()+
", Failures: "+result.failureCount()+
", Errors: "+result.errorCount());
}
getWriter().println();
}
|
回页首
完整生命周期
整个生命周期将在下图显示:
回页首
总结
通过上面的介绍,本文深入地讲解了 JUnit 的核心类和接口,TestCase 的完整生命周期。掌握了这些,开发者有了更加灵活的自用度,可以根据自己特定的项目的特性,定制最合适自身的 MyTestRunner,MyTestResult,MyTestSuite,MyTestListener。从而提高工作效率,发挥 JUnit 的最大作用。
参考资料
学习
------------------------------------------------------------------------------------------------------------------------------------------
JUnit单元测试
主要内容
为什么要进行单元测试
单元测试概述
JUnit简介和经验总结
“测试不是我的工作”
测试是测试部门的责任,我的责任应该关注在写代码上;
测试不是一种技术工作,毫无乐趣可言,请不要骚扰我。我可是一个了不起的SSH程序员
我们有测试人员,有集成/系统/确认测试,他们迟早会发现我的错误,请不要浪费我的时间;
不要侮辱我,我写的程序,怎么可能有错误。测试是完全没必要的。
程序员的难题
开发的模块出现问题,很难定位,已经熬了几个通宵了!!!
后果
软件的质量完全取决于程序员的个人技能和责任心,具有很大的随机性
后期维护成本高昂
1个月的开发,几天的测试,然后花1,2年的时间去修补错误
这个项目我已经维护了3年了
根本原因是软件自身复杂的结构
现实中的发现
编码阶段引入的缺陷远远多于其它阶段
系统测试发现的缺陷大多数是编码缺陷
测试版本频繁,测试和项目进度被无休止的拖延
测试的时间和成本
单元测试
最高的成本收益比
减少联调和后续测试的时间
BUG更容易定位
更有信心去修改老代码
主要内容
为什么要进行单元测试
什么是单元测试(Unit Test)
单元测试测试的软件最小的可执行单元的正确性,即类或方法;
单元测试通常是一段可执行代码,并能验证执行结构是否和预期相等;
单元测试可以是黑盒也可以是白盒,取决于执行方法
单元测试是其他类型测试的基础。不认真,完整的单元测试会导致其他类型测试起不到好的效果
程序员最了解自己的程序单元,最适合做单元测试
传统的重量级的方法学里,UT test case由设计人员在系统设计阶段开发,并用来验证编码人员的工作质量
单元测试任务
单元接口测试
单元局部数据结构测试
单元中重要的执行路径测试
单元的各类错误处理路径测试
单元边界条件测试
单元测试原则
应该尽早地进行软件单元测试。
应该保证单元测试的可重复性。
尽可能地采用测试自动化的手段来支持单元测试活动。
单元测试一定要自动化
只有用代码编写的UT,才能够重现,才能真正节约未来手工测试的时间。
只有用代码编写的UT ,才能做到自动化,才能在软件开发的任何时候都能快速,简单的大批量执行,保证能准确地定位错误,保证不会因为修改而引入新的错误。在系统开发的后期尤为明显。
自动化的UT,才能保证回归测试的有效执行。
单元测试的必要性
带来更大的测试范围
带来团队合作的可能
防止衰退,减少调试
使得重构可行
改进实现设计
当做开发者文档来用
非常有趣
单元测试是成本最低的测试活动
单元测试节约的时间
编写UT代码的时间节约了未来修改/维护低质量代码的时间
学习做UT的时间,是为了以后你可以更好的关注你的代码
如果使用Test-driven的思,单元测试自身就变成设计的一部分,你不会再感到是在浪费时间,编写UT的过程,就是设计的过程
UT快速的定位错误所在,节约了你调试的时间。
程序员的责任
程序员的价值在于和他人合作,开发出高质量的代码,而不是一堆新技术名词堆砌的虫件(bugware)。
程序员必须对自己的代码质量负责,单元测试是对自己代码质量的基本承诺。
程序=UT+CODE
不做单元测试,就会影响团队其他人员的工作。测试人员有权利对没有做过UT的代码说No.不愿意做UT的人,不属于任何团队。
单元测试工具和框架
目前的最流行的单元测试工具是xUnit系列框架,常用的根据语言不同分为JUnit(java),CppUnit(C++),DUnit (Delphi ),NUnit(.net),PhpUnit(Php )等等。
Junit测试框架的第一个和最杰出的应用就是由Erich Gamma (《设计模式》的作者)和Kent Beck(XP(Extreme Programming)的创始人 )提供的开放源代码的JUnit。
JUnit简介和经验总结
Junit框架
JUnit Test case
JUnit assertXXX( )
JUnit Set Up and Tear Down
JUnit Set Up and Tear Down --One time
JUnit Organizing Tests into Test Suites
第一种方法将自动执行testGame所有的testXXX方法
public class TestGame extends TestCase { ...
public static Test suite( ) { return new TestSuite(TestGame.class);
}
}
第二种方法只执行指定的方法
public static Test suite( ) { TestSuite suite = new TestSuite( ); suite.addTest(new TestGame("testCreateFighter"));
suite.addTest(new TestGame("testSameFighters")); return suite;
}
JUnit Exception Handling
JUnit Repeating Tests
JUnit Running Tests Concurrently
JUnit 测试的命名规范
TestSuite处理测试用例有6个规约
测试用例必须是公有类(Public)
测试用例必须继承与TestCase类
测试用例的测试方法必须是公有的( Public )
测试用例的测试方法必须被声明为Void
测试用例中测试方法的前置名词必须是test
测试用例中测试方法无任何传递参数
JUnit经验总结
不要用TestCase的构造函数初始化,而要用setUp()和tearDown()方法。
不要依赖或假定测试运行的顺序,因为JUnit利用Vector保存测试方法。所以不同的平台会按不同的顺序从Vector中取出测试方法。
避免编写有副作用的TestCase。例如:如果随后的测试依赖于某些特定的交易数据,就不要提交交易数据。简单的回滚就可以了。
当继承一个测试类时,记得调用父类的setUp()和tearDown()方法。
将测试代码和工作代码放在一起,一边同步编译和更新。
测试类和测试方法应该有一致的命名方案。如在工作类名前加上test从而形成测试类名。
确保测试与时间无关,不要依赖使用过期的数据进行测试。导致在随后的维护过程中很难重现测试。
编写测试时要考虑国际化的因素。不要仅用母语的Locale进行测试。
尽可能地利用JUnit提供地assert/fail方法以及异常处理的方法,可以使代码更为简洁。
测试要尽可能地小,执行速度快。
单元测试经验
测试驱动开发
编写单元测试用例促进解除模块之间的耦合。先编写测试用例,强迫自己从利于调用者的角度来设计单元,关注单元的接口。为了便于调用和独立测试,必须降低单元和周边环境的耦合程度,单元的可测试性得到加强,模块化程度得到提高。这样单元的可重用性也容易被考虑和提高。
重构
测试用例数量是逐步增加的,软件功能也在此过程中得到增强、更新和优化。当新的需求变化到来时,测试用例被增加或修改,难以适应测试用例的软件单元被重构。经常发生变化的测试用例和软件模块被分离出来,进行重构和优化,使它们更加容易应付需求的变化