单元测试(JUnit)的应用
单元测试(JUnit)的应用
一、 概要
单元测试不仅仅是保证代码在方法级别的正确性,它还能改进设计,易于对代码重构。凡是容易编写单元测试的代码,往往是优秀的设计和松耦合的组件,凡是难于编写单元测试的代码,往往是设计不佳和耦合度高的系统,因此,编写单元测试不仅仅是掌握单元测试框架的用法,更重要的是在编写单元测试的过程中发现设计缺陷,改进系统结构,从而实现良好的可扩展性。
任何一个项目,单元测试应该在详细设计之后开始进行,首先根据详细设计文档进行单元测试用例的编写,编写完成后进行代码开发,代码完成后运行单元测试,如果通过,则该方法可以发布运行,如果不通过需要进行代码改造,再进行单元测试,直到单元测试运行通过为止。
每新增一个功能时的开发流程如图1所示:
图1
本文首先按照上图所示流程进行一个简单功能的开发,同时使用JUnit 4 提供的各种功能开展有效的单元测试,接着对JUnit技术进行了简单的介绍。
二、 软件支持
Ø Eclipse:最为流行的IDE,它全面集成了JUnit,并从版本3.2开始支持 JUnit 4。当然JUnit 并不依赖于任何 IDE。您可以从 http://www.eclipse.org/上下载最新的Eclipse 版本。
Ø JUnit:它的官方网站是 http://www.junit.org/。您可以从上面获取关于 JUnit 的最新消息。如果您和本文一样在 Eclipse 中使用 JUnit,就不必再下载了。
Ø Ant:基于 Java 的开源构建工具,您可以在http://ant.apache.org/ 上得到最新的版本和丰富的文档。Eclipse中已经集成了 Ant。
三、 JUnit4的实际应用例子
3.1 应用例子的程序功能需求
将Java对象名称(每个单词的头字母大写)按照数据库命名的习惯进行格式化,格式化后的数据为小写字母,并且使用下划线分割命名单词,要求对输入数据进行非法验证。
3.2 创建Java工程
首先新建一个Java工程——TestJUnit。打开项目TestJUnit的属性页->选择“JavaBuild Path”子选项->点选“AddLibrary…”按钮 -> 在弹出的“AddLibrary”对话框中选择 JUnit(图2),并在下一页中选择版本4.1后点击“Finish”按钮。
图2
请注意 JDK 的版本:
JUnit 4.1 是基于 Java5 的升级版本,它使用了 Tiger 中的很多新特性来简化原有的使用方式。正因为如此,它并不能直接运行在 JDK1.4.x 版本上。如果您需要在 JDK1.4.x 版本使用 JUnit 的话,请使用 3.8.1 版本。
我们在项目 TestJUnit 根目录下添加一个新目录 testsrc,并把它加入到项目源代码目录中(加入方式见 图3)以存放测试代码,使之与源代码分离。
图3
3.3 编写被测试程序代码
按照程序功能需求所写的代码如下:
import java.util.regex.Matcher; import java.util.regex.Pattern;
/** *对名称、地址等字符串格式的内容进行格式检查 *@authorcws */ publicclass WordDealUtil{
/** *将Java对象名称(每个单词的头字母大写)按照 *数据库命名的习惯进行格式化 *格式化后的数据为小写字母,并且使用下划线分割命名单词 * *例如:employeeInfo经过格式化之后变为employee_info * *@paramnameJava对象名称 */ publicstatic String wordFormat4DB(String name){ Pattern p = Pattern.compile("[A-Z]"); //将给定的正则表达式编译并赋予给Pattern类 Matcher m = p.matcher(name); //生成一个给定命名的Matcher对象 StringBuffer sb = new StringBuffer();
while(m.find()){ m.appendReplacement(sb, "_"+m.group());//Matcher.group()返回匹配到的子字符串 } return m.appendTail(sb).toString().toLowerCase(); } } |
3.3 编写测试代码
一、可以直接添加普通类,编写的单元测试用例:
二、也可以创建Junit测试类
1. 右击test测试包,选择New-->Other...
2. 在窗口中找到Junit,选择Junit TestCase
3. 输入名称(Name),命名规则一般建议采用:类名+Test或者Test+类名。
Browse...选择要测试的类,这里是WordDealUtil,点击“Next>”。
4. 勾选要测试的方法,点击“Finish”。
5. 生成后效果如下
这里import static是引入Assert类中静态属性或静态方法的写法。原来要Assert.fail(),现在只需直接fail()即可,即省略了Assert。
其实不通过Junit新建向导来建立也可以,随便建立一个新类后,只需在方法上加入@Test注解即可。
6.再根据需要更改测试函数内部的代码
importstatic org.junit.Assert.*; import org.junit.*;
publicclass TestWordDealUtil {
@BeforeClass publicstaticvoid setUpBeforeClass() throws Exception { System.out.println("in BeforeClass================"); }
@AfterClass publicstaticvoid tearDownAfterClass() throws Exception { System.out.println("in AfterClass================="); }
@Before publicvoid before() { System.out.println("in Before"); }
@After publicvoid after() { System.out.println("in After"); } //测试wordFormat4DB 一般的处理情况 @Test publicvoid wordFormat4DBNormal(){ String target = "employeeInfo"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_info", result); }
//测试null时的处理情况 @Test publicvoid wordFormat4DBNull(){ String target = null; String result = WordDealUtil.wordFormat4DB(target); assertNull(result); }
//测试空字符串时的处理情况 @Test publicvoid wordFormat4DBEmpty(){ String target = ""; String result = WordDealUtil.wordFormat4DB(target); assertEquals("", result); }
//测试当首字母大写时的情况 @Test publicvoid wordFormat4DBegin(){ String target = "EmployeeInfo"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_info", result); }
//测试当尾字母为大写时的情况 @Test publicvoid wordFormat4DBEnd(){ String target = "employeeInfoA"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_info_a", result); }
//测试多个相连字母大写时的情况 @Test publicvoid wordFormat4DBTogether(){ String target = "employeeAInfo"; String result = WordDealUtil.wordFormat4DB(target); assertEquals("employee_a_info", result); } } |
运行测试,结果如图4所示:
图4
3.4 根据测试结果更改程序
JUnit 运行界面提示我们有两个测试情况未通过——当首字母大写时得到的处理结果与预期的有偏差,造成测试失败(failure);而当测试对 null 的处理结果时,则直接抛出了异常——测试错误(error)。
JUnit 将测试失败的情况分为两种:failure和 error。
Failure:一般由单元测试使用的断言方法判断失败引起,它表示在测试点发现了问题;
Error:则是由代码异常引起,这是测试目的之外的发现,它可能产生于测试代码本身的错误(测试代码也是代码,同样无法保证完全没有缺陷),也可能是被测试代码中的一个隐藏的bug。
显然,被测试代码中并没有对首字母大写和 null 这两种特殊情况进行处理,现在对源代码进行修改,添加对这两种情况的处理,修改后的代码如下:
package com.meritit;
import java.util.regex.Matcher; import java.util.regex.Pattern;
/** *对名称、地址等字符串格式的内容进行格式检查 *@authorlvxg */ publicclass WordDealUtil {
/** *将Java对象名称(每个单词的头字母大写)按照 *数据库命名的习惯进行格式化 *格式化后的数据为小写字母,并且使用下划线分割命名单词 * *例如:employeeInfo经过格式化之后变为employee_info * *@paramnameJava对象名称 */ publicstatic String wordFormat4DB(String name){ if(name == null){ returnnull; } Pattern p = Pattern.compile("[A-Z]"); Matcher m = p.matcher(name); StringBuffer sb = new StringBuffer();
while(m.find()){ if(m.start() != 0) m.appendReplacement(sb, ("_"+m.group()).toLowerCase()); } return m.appendTail(sb).toString().toLowerCase(); } } |
再次运行测试,显示的测试结果如图5所示:
图5
至此,现在的代码已经比较稳定,可以作为API的一部分提供给其它模块使用了,如果测试还没有通过,则继续进行代码改进,一直等到测试完全通过时编码才完成。
您的单元测试代码不是用来证明您是对的,而是为了证明您没有错。因此单元测试的范围要全面,比如对边界值、正常值、错误值的测试;对代码可能出现的问题要全面预测,而这也正是需求分析、详细设计环节中要考虑的。
四、 JUnit的知识点
4.1 注释/元数据
引入库类:import org.junit.*;
1. 说明
@Before | 初始化方法 |
@After | 释放资源 |
@Test | 测试方法,在这里可以测试期望异常和超时时间 |
@Ignore |
忽略的测试方法; 当测试的方法还没有实现,或者测试的方法已经过时,或者在某种条件下才能测试该方法(比如需要一个数据库联接,而在本地测试的时候,数据库并没有连接),那么使用该标签来标示这个方法。 |
@BeforeClass | 针对所有测试,只执行一次,且必须为static void |
@AfterClass | 针对所有测试,只执行一次,且必须为static void |
@RunWith | 指定测试类使用某个运行器 |
@Parameters | 指定测试类的测试数据集合 |
@Rule | 允许灵活添加或重新定义测试类中的每个测试方法的行为 |
@FixMethodOrder | 指定测试方法的执行顺序 |
2. 执行顺序
一个测试类单元测试的执行顺序为:
@BeforeClass –> @Before–> @Test –> @After –> @AfterClass
每一个测试方法的调用顺序为:
@Before –> @Test –>@After
4.2 断言静态类Assert
引入命名空间:import staticorg.junit.Assert.*;
assertArrayEquals(expecteds, actuals) | 查看两个数组是否相等。 |
assertEquals(expected, actual) | 查看两个对象是否相等。类似于字符串比较使用的equals()方法 |
assertNotEquals(first, second) | 查看两个对象是否不相等。 |
assertNull(object) | 查看对象是否为空。 |
assertNotNull(object) | 查看对象是否不为空。 |
assertSame(expected, actual) | 查看两个对象的引用是否相等。类似于使用“==”比较两个对象 |
assertNotSame(unexpected, actual) | 查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象 |
assertTrue(condition) | 查看运行结果是否为true。 |
assertFalse(condition) | 查看运行结果是否为false。 |
assertThat(actual, matcher) | 查看实际值是否满足指定的条件 |
fail() | 让测试失败 |
4.3 多个测试类同时执行
利用org.junit.runner.JUnitCore.runClasses(TestClass1.class, ...)函数,把test case 的类放进去,然后放在main()方法里执行。
比如同时执行测试类TestWordDealUtil、Test2:
publicclass TestMain { publicstaticvoid main(String[] args) { org.junit.runner.JUnitCore.runClasses(TestWordDealUtil.class,Test2.class); }
}
|