About testing
单元测试
单元测试(unit testing)就是指对软件中的最小可测试单元进行检查和验证,根据语言和具体情况的不同,可以是,类、方法等。
单元测试可以由两种方式完成。
人工测试 | 自动测试 |
---|---|
通过main方法(或其他)执行需要测试的单元,然后人为的观察输出确定是否正确称为人工测试。 | 借助工具支持并且利用自动工具执行用例被称为自动测试。 |
消耗时间并单调:由于测试用例是由人力资源执行,所以非常缓慢并乏味。 | 速度快,人力资源需求少 |
可信度较低:人工测试可信度较低是可能由于人工错误导致测试运行时不够精确。 | 可信度更高:自动化测试每次运行时精确地执行相同的操作。 |
非程式化:编写复杂并可以获取隐藏的信息的测试的 话,这样的程序无法编写。 | 程式化:试验员可以编写复杂的测试来显示隐藏信息。 |
Cases
一个基本的开发测试过程包括:编写function code,编写测试case code,编写运行测试code的程序。
在java中,通常需要测试的最小单元是function。
case1
如果我们需要测试下面的plus functoon
1 | package cc.openhome; |
我们可以编写下面的测试:
1 | package test.cc.openhome; |
然后执行测试:
1 | package test.cc.openhome; |
大功告成!然而这是开始。
case2
当然一个类中一般包含多个方法,需要测试的方法也会有多个。
假设我们增加了方法:
1 | package cc.openhome; |
最直接的,我们可以增加一个测试:
1 | package test.cc.openhome; |
我们可以执行测试如下:
1 | package test.cc.openhome; |
可以注意到,这两个测试方法中的判别结果正确与否的逻辑是一样的,我们可以重构将其分离出来:
1 | package test.cc.openhome; |
1 | package test.cc.openhome; |
然而,当前的处理方法是,每次function code增加一个方法,就需要对于的增加一个测试方法,同时还要对应的在测试执行程序(TestRunner)中增加调用测试方法。
case3
为了避免每次都要修改测试执行程序(TestRunner),我们可以将测试抽象为接口,然后将test case改为类(之前是方法级别),实现测试接口:
1 | package test.cc.openhome; |
可以修改测试执行程序(TestRunner)如下:
1 | package test.cc.openhome; |
之后每次需要测试新增的方法,我们不需要再修改TestRunner的代码,只需要添加一个新的testcase的类。
1 | package test.cc.openhome; |
执行测试:
1 | package test.cc.openhome; |
case4
还没结束。。前面的测试只能对单个单个的方法进行测试,不能对test进行组合,设计TestSuite用于组合多个test为一个test。
1 | package test.cc.openhome; |
这样就可以将多个test组合为一个test,然后按照一个test进行执行。
case5
还有问题。。目前每次需要测试一个方法,都需要创建一个类实现test接口。当方法很多时,这是一个很大的问题,费时费力。
最开始的设计时,test是方法级别,由于需要保持TestRunner不总是被修改,我们设计了test接口并将每个test改为了class级别。有没有方法可以保持TestRunner的稳定,并且使每个test回到方法级别?
答案当然是有的,我们可以将所有的test以方法级别都放在一个类中,但是每次具体选择哪个测试方法由参数决定。
1 | package test.cc.openhome; |
有好一些吗?好像是有,至少不用分别建立Test的实作类别了,然而你还可以让测试人员更方便一些。你想在执行测试时,用每个testXXX()方法名称 来建构TestCase的子类别实例,而后运用反射(Reflection)来执行那些testXXX()方法, 让测试人员不用自己去作一些建立物件的动作。为此,你要重构TestSuite:
1 | package test.cc.openhome; |
1 | package test.cc.openhome; |
现在,如果需要写测试,只需要:
1 | package test.cc.openhome; |
我们还可以自由组合各种形式的测试,可以添加一个test方法,也可执行所有test方法,也可混合组合:
1 | package test.cc.openhome; |
case6
在很多情况下,我们希望收集测试的结果,而只是简单的输出到控制台,而是希望进一步处理和操作,这时我们需要设计TestResult用以添加测试结果信息,这里就不在细述。
测试工具
由上面的case可以看出,设计一个强大的可扩展的test系统有很多问题需要考虑,测试工具需要做到:
易于撰写测试
只要撰写testXXX()方法,程式会自动找出并进行测试,事实上,经由设计,在JDK 5以上,还可以使用标注(Annotation)来标示测试方法,JUnit 4.x就可以如此设定。
易于组合测试
可指定测试单一testXXX()方法,可 自动发掘测试案例(Test case)中的所有testXXX()方法,可以自由组合TestSuite等。
让单元测试彼此独立
每个testXXX()方法是封装在一个TestCase的实例中运行,所以单元测试与单元测试间是彼此独立的,如果单元测试有前置状态,你也可以利用setUp()、tearDown()来使之处于前置状态。
自动收集并产生结果
经由适当的组合,你可以一次运行所指定的任意个单元测试,过程中会自动收集结果,最后可用指定的方式(Test runner)来呈现结果,事实上,藉由Ant或Maven之类的工作,你还可以进一步产生各种类型的测试报告并邮寄至相关人等。
JUnit
JUnit是个单元测试(Unit test)框架,单元测试指的是测试一个工作单元(a unit of work)的行为。举例来说,对于建筑桥墩而言,一个螺丝钉、一根钢筋、一条钢索甚至一公斤的水泥等,都可谓是一个工作单元,验证这些工作单元行为或功能(硬度、张力等)是否符合预期,方可确保最后桥墩安全无虞。
测试一个单元,基本上要与其它的单元独立,否则你会在同时测试两个单元的正确 性,或是两个单元之间的合作行为。就软体测试而言,或支援物件导向的程式而言,例如Java,「通常」单元测试指的是测试某个方法,你给予该方法某些输入,预期该方法会产生某种输出,例如传回预期的值、产生预期的档案、新增预期的资料等。
JUnit 促进了“先测试后编码”的理念,强调建立测试数据的一段代码,可以先测试,然后再应用。这个方法就好比“测试一点,编码一点,测试一点,编码一点……”,增加了程序员的产量和程序的稳定性,可以减少程序员的压力和花费在排错上的时间。
- JUnit 是一个开放的资源框架,用于编写和运行测试。
- 提供注释来识别测试方法。
- 提供断言来测试预期结果。
- 提供测试运行来运行测试。
- JUnit 测试允许你编写代码更快,并能提高质量。
- JUnit 优雅简洁。没那么复杂,花费时间较少。
- JUnit 测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。
- JUnit 测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。
- JUnit 在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。
JUnit 3 vs JUnit 4
Junit也处于不断发展的过程中,虽然基本思想没有大的变化,但是具体实现和使用,从JUnit 3到JUnit 4还是有一些变化。
上面描述的各个case的发展的最后面,就是JUnit 3的主要框架和思想了,当然JUnit 3提供了更完善和强大的设计和支持。
JUnit 3与JUnit 4主要区别,参考JUnit测试框架之JUnit3和JUnit4使用区别的总结
- 在JUnit3中需要继承TestCase类,但在JUnit4中已经不需要继承TestCase
- 在JUnit3中需要覆盖TestCase中的setUp和tearDown方法,其中setUp方法会在测试执行前被调用以完成初始化工作,而tearDown方法则在结束测试结果时被调用,用于释放测试使用中的资源,而在JUnit4中,只需要在方法前加上@Before,就代表这个方法用于初始化操作,如上面的beforeDoTest方法,方法名是随意的
- 在JUnit3中对某个方法进行测试时,测试方法的命令是固定的,例如对addBook这个方法进行测试,需要编写名字为tetAddBook的测试方法,而在JUnit4中没有方法命令的约束,例如对addBook这个方法进行测试,那么可以编写addBookToLibrary的方法,然后在这个方法的前面加上@Test,这就代表这个方法是测试用例中的测试方法
- 编写JUnit4的测试用例和编写一个普通的类没有什么区别,只是需要加上Annotation指定要测试的方法,这种松偶合的设计理念相当优秀,能很好把测试分离出来.使用JUnit4的Annotation功能,需要JDK 1.5或以上版本
- JUnit 4还提供了其他基于注解的强大的功能的支持。
后面主要介绍JUnit 4的用法,当然很多也是从JUnit 3继承下来的。