基于JUnit实现代码级接口测试
对于代码级测试的知识,前面三节的内容,已经足够。所谓的单元测试框架,无非实现了更加简洁的测试驱动和断言,充分利用了编程语言特性帮助测试开发人员提高效率,进而通过丰富一些实用功能,让整个白盒测试过程更加流畅,仅此而已。
实验简介
理解了代码级接口测试的基本原理与实现方式后,我们再来理解单元测试框架将会非常容易,言下之意也是想告诉大家,对于代码级测试的知识,前面三节的内容,已经足够。所谓的单元测试框架,无非实现了更加简洁的测试驱动和断言,充分利用了编程语言特性帮助测试开发人员提高效率,进而通过丰富一些实用功能,让整个白盒测试过程更加流畅,仅此而已。
实验目的
(1) 掌握JUnit测试框架的配置和使用。
(2) 掌握JUnit测试框架的断言和注解。
(3) 利用JUnit测试框架的高级功能及应用。
实验流程
1. 直接在项目中引入JUnit
(1) 在Eclipse中的项目CodeTest名称上,点击“右键”->“Properties”,打开项目属性对话框,进入“Java Build Path”节点,如图。
(2) 在“Libraries”标签页上,点击“Add Library”按钮,打开“Add Library”对话框,选择“JUnit”,如图。
(3) 点击“ Next”,选择JUnit4的版本,完成即可在代码中使用JUnit框架了,如图。
2. 下载jar包并引入项目中
(1) 访问网址:http://junit.org/junit4/ 下载junit.jar包。
(2) 在项目CodeTest根目录下,创建目录“lib”,当然也可以命名为其它,后续我们的代码中也会导入很多其它的jar包,都统一放置于本目录中。
(3) 在项目中直接导入该jar包,在项目属性对话框,进入“Java Build Path”节点,点击“Add Jar”按钮,将该jar包导入项目中,如图。
3. 使用JUnit 3的语法规则来书写测试脚本
package com.woniuxy.junit;
//导入JUnit相关类和方法 import com.woniuxy.compare.StringHandle; import static org.junit.Assert.assertArrayEquals; import junit.framework.TestCase;
//该测试类必须继承自TestCase类 public class JUnit3Test extends TestCase { // 各个测试方法必须以test开头 public void test_splitString() { StringHandle stringHandle = new StringHandle(); String source = "333,111,222,666"; Integer[] expect = { 333, 111, 222, 666 }; Integer[] actual = stringHandle.splitString(source, ","); assertArrayEquals(expect, actual); // 断言,用于结果比较 }
// 各个测试方法名称必须以test开头 public void test_isNumber() { StringHandle stringHandle = new StringHandle(); String source = "123.45"; boolean actual = stringHandle.isNumber(source); assertTrue(actual); // 断言,用于结果判断 } } |
注意,在使用JUnit 3的方法规则书写测试用例时,必须满足两个基本要求:
(1) 测试类必须继承自junit.framework.TestCase类
(2) 每个测试方法名称必须以test开头,通常我们以test_后面跟上被测方法名称来命名测试方法。
4. 用JUnit 4的规则重写上述测试用例
package com.woniuxy.junit;
import org.junit.Test; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.woniuxy.compare.StringHandle;
public class JUnit4Test {
@Test // 只需要声明@Test注解即可 public void splitString() { StringHandle stringHandle = new StringHandle(); String source = "333,111,222,666"; Integer[] expect = {333, 111, 222, 666}; Integer[] actual = stringHandle.splitString(source, ","); assertArrayEquals(expect, actual); // 断言,用于结果判断 } @Test // 只需要声明@Test注解即可 public void isNumber() { StringHandle stringHandle = new StringHandle(); String source = "123.45"; boolean actual = stringHandle.isNumber(source); assertTrue(actual); // 断言用于结果判断 assertEquals(true, actual); // 也可以使用assertEquals断言 } } |
根据JUnit 4的规则,我们可以看到如下一些变化:
(1) 测试类不需要继承TestCase,灵活了很多。
(2) 测试方法名称不需要以test开头,只需要加上@Test注解指明该方法是JUnit测试方法即可。
事实上,在后续的章节中,我们将会看到这两点看似很小的改动,事实上是非常有用的。
5. 执行JUnit测试用例
该结果说明有两个测试用例被执行,并且两个测试用例均成功执行。没有出现错误,如果出现错误或异常,那么我们可以在JUnit视图的下半部分“Failure Trace”中进行定位。
6. 设定用例执行顺序
在上述执行结果中,我们可以看到,isNumber的测试用例在代码中是后于splitString的,但是执行时却是先执行的,那么有没有什么方法可以让我们自己定义执行顺序呢?在相对早期的JUnit版本中,这是无法做到的,只能按照JUnit的方式进行,但是在JUnit 4.11及后续版本中,终于解决了该问题。
我们可以使用JUnit中的一个特殊的注解:@FixMethodOrder来指定测试用例的执行顺序。该注释通过设定一个排序参数即可完成,该排序参数通过MethodSorters对象进行调用,有三种排序方式:
(1) MethodSorters.DEFAULT:默认排序,也就是不使用该注解的情况。
(2) MethodSorters.NAME_ASCENDING:按字母顺序升序排列。
(3) MethodSorters.JVM:按代码中JVM中的加载顺序排列。
所以,针对上述测试代码,如果我们想让splitString先于isNumber的测试用例执行,那么我们可以将代码修改为:
package com.woniuxy.junit;
import org.junit.Test; import org.junit.runners.MethodSorters; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.FixMethodOrder; import com.woniuxy.compare.StringHandle;
// 使用注解,设定执行顺序为按字母表升序执行 @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class JUnit4Test {
@Test // 只需要声明@Test注解即可 public void test_001_splitString() { StringHandle stringHandle = new StringHandle(); String source = "333,111,222,666"; Integer[] expect = {333, 111, 222, 666}; Integer[] actual = stringHandle.splitString(source, ","); assertArrayEquals(expect, actual); // 断言,用于结果判断 } @Test // 只需要声明@Test注解即可 public void test_002_isNumber() { StringHandle stringHandle = new StringHandle(); String source = "123.45"; boolean actual = stringHandle.isNumber(source); assertTrue(actual); // 断言用于结果判断 assertEquals(true, actual); // 也可以使用assertEquals断言 } } |
7. JUnit的断言
我们知道,判断一个测试用例是否通过的过程其实就是一个期望结果与实际结果比较的过程。在JUnit中使用断言(Assert)机制来进行判断,免去了使用if … else …分支语句进行结果判断的麻烦。更可贵的时,如果断言失败,JUnit将会输出期望结果与实际结果的值,便于我们快速定位问题。这就是断言的价值所在。那么,JUnit中都有哪些可用的断言呢?
在JUnit中,有两个断言类,一个是junit.framework.Assert,另外一个是org.junit.Assert。前者是专门为了兼容JUnit 3而刻意保留的,后者则是JUnit 4新增的,如果没有历史遗留原因,建议直接使用JUnit 4断言类中的各类断言,在该类中,主要包含了如下断言方法:
(1) assertTrue(boolean condition)
用法:判断参数是否为布尔“真”,为真则通过,否则失败。
(2) assertFalse(boolean condition)
用法:判断参数是否为布尔“假”,为假则通过,否则失败。
(3) assertEquals(Object expected, Object actual)
用法:判断两个参数值是否相等,支持所有基本数据类型的比较。
(4) assertEquals(double expected, double actual, double delta)
用法:该断言比较特殊,专门用于比较浮点型数据,并使用了误差范围,如以下断言将通过:assertEquals(12.5, 12.3, 0.5),误差0.5表示正负0.5均在允许范围。
(5) assertArrayEquals(Object[] expecteds, Object[] actuals)
用法:比较两个数组是否相同,相同的条件为:长度相同,相同下标的值也相同。
(6) assertNotNull(Object object)
用法:参数为一个非空对象,非空则断言正确,不适用于基本数据类型。
(7) assertNull(Object object)
用法:参数为一个空对象,空则断言正确,不适用于基本数据类型。
(8) assertSame(Object expected, Object actual)
用法:比较两个对象是否相同,相同则断言正确。对象相同的条件是指向同一块内存,该断言不适用于基本数据类型。
(9) assertNotSame(Object unexpected, Object actual)
用法:比较两个对象是否相同,不同则断言正确。
8. assertThat断言
assertThat(T actual, Matcher<T> matcher)
用法:该断言较为特殊,无法直接在JUnit中使用,必须配合Matcher匹配器使用才可以进行断言,Matcher匹配器包含在另外一个扩展框架,叫hamcrest中。我们可以进入其官方页面http://hamcrest.org/下载适用的版本,并导入到项目中便可使用。示例代码如下:
import static org.junit.Assert.*; import org.junit.Test; import static org.hamcrest.Matchers.*;
public class SpecialJUnitUsage { @Test public void assertThat_Test() { double d = 100; assertThat(d, is(100.0)); assertThat(d, lessThan(200.0)); assertThat(d, greaterThan(20.0)); assertThat(d, closeTo(100.0, 1.0)); } } |
9. JUnit的注解
JUnit 4中除了常用的@Test注解外,还包括如下注解:
(1) @BeforeClass: 测试类运行前准备环境,一个测试类在运行测试方法之前运行一次。
(2) @AfterClass: 测试类运行后清除环境,一个测试类在运行完所有测试方法后运行一次。
(3) @Before: 每个测试用例运行前运行,有多少个测试用例,就会运行多少次。
(4) @After: 每个测试用例运行后运行,有多少个测试用例,就会运行多少次。
(5) @Test: 具体的测试用例。
(6) @Ignore: 该测试用例将被忽略。
(7) @Rule: 定义一些规则,如超时时间,异常捕获等。
下面通过两段代码来分别理解以上几个注解:
(1) @BeforeClass和@AfterClass
其特点为在一个测试类中永远只会被运行一次。
package com.woniuxy.junit;
import static org.junit.Assert.*; import org.junit.*; import com.woniuxy.compare.StringHandle;
public class JUnitAnnotation {
public static StringHandle stringHandle; @BeforeClass public static void classInit() { System.out.println("为测试集进行实例化."); stringHandle = new StringHandle(); } @AfterClass public static void classFree() { System.out.println("为测试集释放资源."); stringHandle = null; } @Test public void splitString() { String source = "333,111,222,666"; Integer[] expect = {333, 111, 222, 666}; Integer[] actual = stringHandle.splitString(source, ","); assertArrayEquals(expect, actual); } @Test public void isNumber() { String source = "123.45"; boolean actual = stringHandle.isNumber(source); assertEquals(true, actual); } } |
在上述代码中,我们利用注解@BeforeClass和@AfterClass,来帮助实例化StringHandle类和释放该实例的内存资源。而这类操作通常在一个类中只需要处理一次,我们就没有必要在每个测试用例中去实例化一个StringHandle类了,这可以帮助节省内存消耗。当然,所有的一次性的操作,我们都可以放置在被@BeforeClass注解的方法中进行。
当然,针对注解@Before和@After,其特点则是保证每一个测试用例都会运行一次。针对不同的测试用例,我们需要设置一些前置后置的共用条件时,可以如此来处理。比如我们需要去一个论坛发帖和回帖,则他们共同的前置条件都是必须要先登录该论坛。
(2) @Rule
为测试用例设定运行规则,如超时和异常捕获,请看如下代码片段:
// 如果代码运行的时间超过3秒,则直接报错,可以用于检查代码的运行效率 @Test(timeout = 3000) public void testTimeout() throws InterruptedException { Thread.sleep(2000); // 试试暂停4秒钟 assertTrue(true); }
// 希望代码运行期间可以抛出异常,如果没有按照预期抛出,则测试失败 @Test(expected = ArithmeticException.class) public void testException(){ int i = 9 / 0; // 试试除以3 } |
10. 参数化
参数化究竟有何神秘之处?让我们先从一个简单的案例说起,我们在对StringHandle类中的splitString和isNumber两个方法进行测试时,根据测试用例设计方法,我们必然会想到要进行多种有效等价类和无效等价类的测试,一个测试方法显然不够用。比如isNumber,我们自然想到要对如下输入进行测试以求全面:正数,负数,小数,字符串,数字加字符串,特殊字符等,那么按照之前的用法,我们就得为其准备至少6个测试方法来进行测试和断言,这很显然将增加测试代码的维护成本。JUnit的参数化便是用来解决这一问题的。
参数化机制简单说来就是“实现测试代码和测试数据的分离”,那么,对一个被测方法来说,我们只需要实现一个测试用例代码,将测试数据进行重用即可。这一原理也普遍适用于所有黑盒类的自动化测试工具中。
现在,我们就用参数化机制来对StringHandle类中的splitString和isNumber两个方法进行改造:
(1) 对isNumber进行参数化改造:
package com.woniuxy.junit;
import com.woniuxy.compare.StringHandle; import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class) // 使用参数化运行器 public class JUnitParamIsNumber { public String source; public boolean result;
// 构造函数 public JUnitParamIsNumber(String source, boolean result){ this.source = source; this.result = result; }
@Parameters // 指定该方法为参数生成器 @SuppressWarnings("unchecked") // 忽略警告信息 public static Collection getParamters() { // 输入值与结果必须与构造函数定义一一对应 Object[][] object = {{"12345", true}, {"TT123", false}, {"", false}, {null, false}, {"!@#$%^&", false}}; return Arrays.asList(object); }
@Test public void isNumber_Param() { StringHandle sh = new StringHandle(); boolean result = sh.isNumber(this.source); assertEquals(this.result, result); } } |
(2) 对splitString进行参数化改造:
package com.woniuxy.junit;
import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; import org.junit.Test; import com.woniuxy.compare.StringHandle; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class) // 使用参数化运行器 public class JUnitParamSplitString { public String source; public Integer[] result; // 构造函数 public JUnitParamSplitString(String source, Integer[] result){ this.source = source; this.result = result; } @Parameters @SuppressWarnings("unchecked") public static Collection getParamters() { Integer[] result1 = {11,22,33}; // 对于复杂类型的参数值,先定义 Integer[] result2 = {-22,-33,-44}; // 由于参数包含不同数据类型,所以我们使用Object超类来定义,这样可以兼容所有类型 Object[][] object = {{"11,22,33", result1}, {"-22,-33,-44", result2}}; return Arrays.asList(object); }
@Test public void splitString_Param() { StringHandle sh = new StringHandle(); Integer[] splitResult = sh.splitString(this.source, ","); assertArrayEquals(this.result, splitResult); } } |
11. 测试集
到目前为止,我们仍然在对单个Test Fixture进行测试。很显然,实际使用中不可能只是这样手工的一个一个Fixture来运行。既然我们是在写自动化测试代码,当然也希望代码可以全自动运行,否则就失去了自动化测试本身的价值了。JUnit使用Test Suite(测试集)运行器来完成自动化运行,其核心作用就是通过指定Test Fixture类名来完成调用,代码如下:
package com.woniuxy.junit;
import org.junit.runner.RunWith; import org.junit.runners.Suite;
@RunWith(Suite.class) @Suite.SuiteClasses( { JUnit4Test.class, JUnitAnnotation.class, JUnitParamIsNumber.class, JUnitParamSplitString.class })
public class ArrayCompareTestSuite { // Do Nothing } |
上述代码的运行结果如下:
12. 关于单元测试框架的思考
事实上,JUnit框架还有更多的功能,比如参数化,假设机制,测试管理等功能。但是笔者不准备再跟大家进行介绍了,原因非常简单,这些功能都没有什么实际用处,或者为了适应JUnit的这些用法,我们有时候反而花了更多时间来设计测试驱动程序,而并不是把注意力放在测试用例的设计和思考上。
在单元测试领域,除了测试驱动程序的框架以外,还有一类框架,专门用于隔离测试环境,模拟被调用的其它方法或函数。比如在Java领域有JMock这样的框架,在C++领域有GoogleMock等。这类框架在很多项目中几乎是运用不起来了,对代码的结构设计和规范设计要求非常高。事实上,笔者的个人经验表明,95%以上的项目,我们完全没有必要在进行代码级测试时对调用的其它接口进行隔离,这非常消耗精力和时间,同时,把本来可以一次性完成单元测试和集成测试的理想状态人为了进行隔离,基本上是多此一举,得不偿失。虽然经典测试理论告诉我们,做单元测试必须要隔离,但是我们更强调可行性,效率,而不是追求与理论的匹配度。在这一点上,很多读者可能在自己的项目团队中根本不会用到,所以不一定会有什么感触,这样最好,说明大家没有在一条错误的道路上行走。当然,大家如果对Mock对象的运行原理或者JMock框架的使用想要有更深入的理解,可以关注笔者在蜗牛学院上发表的文章。
我们再回过头来说说XUnit单元测试框架,笔者的结论是,纯粹基于代码级的测试层面来说,所有的单元测试框架都是纸老虎,基本没有价值。我们试着以JUnit举例,来看看JUnit所提供的一些功能,我们如何自行来实现:
(1) 各类断言:没有什么断言,是if…else…解决不了的问题。
(2) @BeforeClass和@AfterClass:在主调方法中,调用一次还是N次,自行决定不是很好?
(3) @Before和@After:设置好测试用例的前置后置条件,给多个类中的测试用例共同调用不是更有重用性?我们完全可以通过调用一个自定义的方法,按需调用,更加灵活。
(4) @Rule:Rule中的超时机制完全可以通过统计代码运行之前和运行结束后的时间差来得出,进而根据运行时间来判断。Rule中的异常机制更加简单,用try…catch…直接就可以处理,而且更加灵活,更加多样化,何必拘泥于JUnit的规则呢?
(5) @Ignore:既然这个测试用例不准备执行,那就直接注释掉或者在Main方法中不调用就好了嘛。
(6) 参数化:Junit的参数化机制设置了过多的严格的条件,没有任何实用价值。在进行自动化的过程中,数据驱动的测试已经成为自动化测试框架的标准配置,技术上没有任何难度。
(7) Hamcrest框架:笔者实在找不出可以用到它的理由,一个简单的“d < 200”的基本比较运算就可以搞定的事情,我们还非得写成“assertThat(d, lessThan(200.0))”,我们实在找不到一个用它的理由。
(8) XML报表:JUnit可以导出XML格式的测试报告,该报告的内容类似这样。
这样的报告都关注在细节上,对我们关注测试的细节没有多少实际价值,看着头疼。我们为什么不定制更加友好,更加关注整体的报表呢?比如在后面的章节中,我们会看到的Ecl-Emma生成的报表,或者在CBT框架中我们自己定义输出的HTML报告等,看着就更加友好,也更能够便于分析和解决问题。
说了这么多,笔者最想告诉大家的是,我们要完成一个单元测试框架,是非常容易的事情,针对我们日常的自动化测试实践,结合我们的实际项目,完成更加有针对性的框架设计和优化,比起去学习一些别人的框架或者开源的框架,要来得有实用价值得多。无论是代码级的自动化测试框架,还是基于协议的或者基于界面的测试框架,都存在这样的问题。本书会在后续各章节中,详细为大家讲解所有自动化测试的工作原理,以及不使用框架如何完成测试开发。但是作为一本以实战性为主的测试开发教程,笔者仍然会给大家讲解市面上常见的一些自动化测试框架或工具的使用,作为对原理的深入,也帮助读者朋友快速上手。但是笔者的观点始终是鲜明的:框架或工具,有用的没几个。
思考练习
(1) 如果需要自行实现一个代码级接口测试框架,你觉得是否有可行性?试着完成一个方案。
(2) 将上述方案用代码实现,完成自己的测试框架。