JUnit是一个开源的java自动化单元测试框架。由 Erich Gamma 和 Kent Beck 与1997年开发完成。 Erich Gamma 是 GOF的作者 之一;Kent Beck 则在 XP 中有重要的贡献。Junit常常被开发者用来做单元测试的自动化测试框架。但是按照测试人员的角度来说,Junit更严格来讲是一个测试驱动器,我们不仅仅可以用其来做单元测试,也是可以以其为核心来做集成测试、系统测试。很多人分析Junit喜欢从开发角度从设计模式来分析,但是对于测试人员来说需要的是从中学习自动化测试框架的基本组成部分与结构模式。学习Junit不仅仅是学习其用法,更要从中学习一个自动化测试框架的有哪些组成部分?各个组成部分之间是如何协调运作来形成一个可扩展稳健的自动化测试框架。Junit可谓“麻雀虽小,五脏俱全。”,其包含一个自动化测试框架所有的要素部分,很多的测试框架,千变万化其实都离不开一个测试框架必有的组件与模式。本系列尝试从一个手工测用例转化为自动化执行以后的模式,来总结出一个自动化测试所必须的一个组成部分,并借用Junit来说明Junit中是如何实现这几个部分的。
基本概述
让我们从平时做测试的一个简单情形开始。想想平时我们是如何做测试的,或者说是如何执行测试过程的?是不是有用例?我们编写了用例,在用例里面说明了测试预置环境,测试输出数据,执行步骤,和预期的结果。例如:
拿到这个用例,是不是要一步步按照用例的说明来操作。在人工测试的情况下类似如下的情况,
在这个过程当中,测试人员是执行者,读取测试用例内容,操作被测对象,并判断结果是否正确。
那么,如果要把上述的过程自动化需要如何做呢?在自动化的过程中,人不在了,那么谁来读取测试用例呢?准确点说由谁来读取测试用例的文件,并逐条执行里面的用例呢?首先想到就是写一个程序,此程序能读取文件,并识别里面的逐条的测试用例,然后执行。这样的一个程序叫做测试执行器(Test Runner)。
有了执行器以后,又面临一个问题,在人工操过程中,人是可以识别文件里的文本用例,读懂它的步骤然后操作,但执行器就不行了,它是程序他无法识别文本的意思。程序识别程序,那么自然而然想到的方法就是把测试用例方法化,即把测试用例用程序的方法来表示,即测试方法(Test Method),因此执行器执行用例,就转化为执行器调用方法的过程。
我们进一步观察,测试方法,实时上,一个方法()Test Method 就代表一个测试用例,一个测试用例主要包含的重要信息,就是用例的执行环境是什么,执行步骤,预期值是什么,等等。类似的测试方法也需要包含四个步骤,称为测试四步骤(Four-Phase Test)
- 环境设置
- SUT调用
- 结果验证
- 环境清理
回顾之前所述,在测当中一个重要的步骤就是验证,手动测试中,肉眼查看运行结果并和预期对比来得出测试是否通过的结果。在自动化过程中自然需要这个过程,让测试方法自动验证测试结果呢?这部自动化测试中称为断言(Assert)。
在整个的测试过程当中还包过如何过滤测试方法、、测试数据管理、最终测试结果报告等等。
如上述,我们总结一下,在自动化测试当中需要涉及的部分包括:
- 测试执行器(Runner):读取测试用例、执行测试用例、测试报告输出
- 测试输入数据管理:如果测试需要外部输入数据,这些数据如何组织管理
- 环境设置(setUp tearDown):主要设置用例执行前的环境、测试结束后清理测试环境;
- 结果断言(Assert):如何来判断测试结果是否符合预期
- 测试组织与流程控制:如何来组织测试用例,测试用例的过程如何控制
那么作为一个自动化的测试框架,Junit如何对上述各个组成部分进行处理呢,我们将从一个Junti入门例子开始,先对Junit做大致介绍,然后逐一演示Junit是如何实现上述各个部分的?只要大家理解了Junit对上述几个部分是怎么做的,后续学习其他测试驱动器就可以快速入手,比如TestNG-在讲完Junit后,我们会针对TestNG如何实现上是问题做简单说明,只要大家牢固掌握Junit实现上述问题的本质,学习testNG也就是分分钟的事情。
在Eclipse中使用JUnit4
在开始之前我们先把Junit引入到我们的Eclipse的项目中来,在Eclipse项目右键选中,如下图所示
在弹出的属性窗口中,首先在左边选择“Java Build Path”,然后到右上选择“Libraries”标签,之后在最右边点击“Add Library…”按钮,如下图所示:
然后在新弹出的对话框中选择JUnit4并点击确定,如上图所示,JUnit4软件包就被包含进我们这个项目了。
Junit 入门例子
假设我们有一个计算器类,包含两个方法,add和multiply。
public class Calculator {
public int add(int one, int another) {
return one + another;
}
public int multiply(int one, int another) {
return one * another;
}
}
对这两个方法,我们写两条happy path的测试用例:
要把这两条测试用例自动化执行,要涉及前面所述的几个步骤,首先是要测试用例脚本化,把测试用例的步骤用代码方法来表示,我们建一个类叫CalculatorTest,包含这两条测试用例:
public class CalculatorTest {
public void testAdd(){
Calculator calculator = new Calculator();
int sum = calculator.add(6,7);
if(sum == 13) {
System.out.println("add() SUCCESS!");
} else {
System.out.println("add() FAIL!");
}
}
public void testMultiply(){
Calculator calculator = new Calculator();
int product = calculator.multiply(8,9);
if (product == 56) {
System.out.println("multiply() SUCCESS!");
} else {
System.out.println("multiply() FAILs!");
}
}
}
测试方法是不是要执行器来执行?如果我没有Junit这样的框架,我们怎么执行上述的测试方法呢?如下也许是一种方法:
public class Client {
public static void main(String[] args) {
CalculatorTest test = new CalculatorTest();
test.testAdd();
test.testMultiply();
}
}
然后再通过某种方式运行这个main 方法,查看打印的输出,来验证测试是成功还是失败。想想一下,如果我们有很多的类,每个类都有很多方法,每个方法都要写很多的分支来判断结果,而且还要写个main方法一个个去调用各个类方法,明显是不可行呢?如果有一个程序--执行器,能够自动创建一个测试类,并自动调用里面的各个测试方法那么就简化很多了。Junit框架就提供了这样的功能,Junit内置的执行器,能够读取一个类,并执行里面的测试方法,要让执行者自动找到测试方法并调用,必须要有一定的约定,比如告诉执行器说用test开头的方法就是测试方法(Junit 3所采用的方法)或者给测试方法某个标注来表示测试方法,那么执行器就会按着这个约定去调用类里的有特定命名方式或者有特定标志的测试方法。我们来Junit来改写上述的测试:
public class CalculatorTest {
@Test
public void testAdd(){
Calculator calculator = new Calculator();
int sum = calculator.add(6,7);
if(sum == 13) {
System.out.println("add() SUCCESS!");
} else {
System.out.println("add() FAIL!");
}
}
@Test
public void testMultiply(){
Calculator calculator = new Calculator();
int product = calculator.multiply(8,9);
if (product == 56) {
System.out.println("multiply() SUCCESS!");
} else {
System.out.println("multiply() FAILs!");
}
}
}
大家注意到每个测试方多了一个注解@Test,这个注解的作用的就是告诉Junit的Test Runner 这是一个测试方法,Test Runner就会调用这个方法。这时候你就不用自己写一个main来执行这些方法,在Eclipse下右键 Run as Junit Test,Junit的测试执行器就会自动运行里面的测试方法。
还有两个地方:一个是设置部分,每个测试方法都创建了一个Calculator,这个重复的代码是否可以抽取出来呢?还有判断结果部分分支复杂,能否简化了?Junit提供fixture和断言Assert相关方案来解决这两个问题,看再一次用Junit简化后的测试代码:
public class CalculatorTest {
// fixture部分
public Calculator calculator;
@Before
public void setUp(){
calculator = new Calculator();
}
//测试方法
@Test
public void testAdd(){
calculator = new Calculator();
int sum = calculator.add(6,7);
//断言部分
Assert.assertEquals(13, sum);
}
@Test
public void testMultiply(){
calculator = new Calculator();
int product = calculator.multiply(8,9);
//断言部分
Assert.assertEquals(72, product);
}
}
上述@Berfore 表示此方法在每个测试方法运行之前运行一次。Assert.assertEquals判断实际结果与预期是否一致。至此,这就是一个简单完整的Junit测试代码。用Junit执行器运行后有如下图的测试结果展示:
上述的代码中有疑问的地方先不管,这只是让大家体会一些一个Junit测试的整体流程,各个注解后面会有详细介绍。这边讲一些测试的统一结构和命名问题。
统一的测试结构
回顾我们的第一个Junit测试,可以总结出一个良好的测试方法包含四个步骤,就是上回我们提到的:
- 设置:称为Fixture
- 执行:掉测试方法
- 断言:判断结果是否预期
- 清理:清理测试,相关例子后续会看到
极力建议再写测试过程中中应该在每个阶段开始时做好注释。
测试方法命名规则
Junit4之前,注解,Junit必须采用一种方法来区分普通的方法和测试方法,当时采用的办法标识 以test为前缀的方法名、public、非static、void、无参的方法为测试方法,因此,测试方法名称都testFoo,testBar诸如此类。
但是Junit设计师认为这种的命名方式有时候无法真实揭示测试方法真实的用意,当一个方法对应的测试方法多了以后,甚至还会造成一些混乱,因此在Junit采用@Test注解来标识测试方法后,Junit取消测试方法名称必须test开头的规定,而是可以随意命名,但是大部分人还是以test开头。
后面有人提出 行为表达式(Behavior-expressing patterns)的命名方式:
[UnitOfWork_StateUnderTest]
下划线可以用with、if 等此替换。这种方式的作用是希望能让测试用例和代码文档一样易于阅读,比如上述的代码写久了,回头去看不知道方法testMultiply在乘积溢出情况下会返回什么,如果你的测试名称是这么写的:
multiplyOverflowWithException()
那么看测试方法的人就知道这测试方法,在溢出时会抛异常
集体怎么命名取决与你们的团队是如何约定,看个人喜好。
总结
我们讨论手工测试自动化后,需要处理的几个步骤,包括测试方法的读取与执行、测试执行环境的设置、测试结果的判断、测试报告、测试数据的管理、测试用例的组织等方面,一般的自动化测试框架要解决也就这几个方面,对于Junit testNg无不如此。对于Junit 我们将从测试执行、断言、执行器、异常测试、测试组织等几个大方面详细介绍它的使用