关于Java单元测试,你需要知道的一切
简介
单元测试是先mock一些正常边界异常条件来对接口进行操作,并且期望接口返回什么内容,最后接口实现了之后再重新测试一遍。
TDD:Test-Driven Development,测试驱动开发模式——旨在强调在开发功能代码之前,先编写测试代码。开发未动,测试先行。
为什么我们要进行单元测试:
最后才修改一个 bug 的代价是在 bug 产生时修改它的代价的10倍。——《快速软件开发》
测试任何可能的错误。单元测试不是用来证明您是对的,而是为了证明您没有错。
单元测试是编写测试代码,用来检测特定的、明确的、细颗粒的功能。单元测试并不一定保证程序功能是正确的,更不保证整体业务是准备的。
单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构之后的正确性。
JUNIT
JUnit 是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。JUnit 测试是程序员测试,即所谓白盒测试。它是一个 Java 语言的测试框架,多数 Java 的开发环境都已经集成了 JUnit 作为单元测试的工具。
特性
- 用于测试期望结果的断言(Assertion)
- 用于共享共同测试数据的测试工具
- 用于方便的组织和运行测试的测试套件
- 图形和文本的测试运行器
基本流程
- 写测试类并继承 TestCase 类
- 写测试方法 testXXX(), 测试方法必须以 test 开头
- 写测试套件类,将 test case 加入到 test suite, 如果没有为 test case 写 test suite 的话,系统会默认为每个 test case 生成一个 test suite
- 运行JUnit Test 进行测试
剖析概念
TestXXX
- TestCase(测试用例)
当一个类继承 JUnit 的 TestCase 类,即成为一个测试类,而且,测试类中的方法必须以 test 开头,比如:testAdd() 等。
- TestSuite(测试套件)
TestSuite 是一组测试,目的在于将相关的测试用例归入一组。当执行一个 Test Suite 时,就会执行组内所有的测试方法,这就避免了繁琐的测试步骤。当然,如果没有为 test case 写 test suite 的话,系统会默认为每个 test case 生成一个 test suite。
Assert(断言)
Assert 用于检查条件是否成立,当条件成立则 Assert 方法通过,否则会抛出异常。例如,Assert.assertEquals(3, result); 判断 result 是否跟期望的3想等,如果想等则通过,否则测试失败。
主要有如下几个断言方法:
- assertTrue/False():判断一个条件是 true 还是 false。
- fail():失败,可以有消息,也可以没有消息。
- assertEquals():判断是否想等,可以指定输出错误信息。注意不同数据类型所使用的 assertEquals 方法参数有所不同。
- assertNotNull/Null():判断一个对象是否为空。
- assertSame/NotSame():判断两个对象是否指向同一个对象。
- failNotSame/failNotEquals():当不指向同一个内存地址或者不相等的时候,输出错误信息。错误信息为指定的格式。
- setUp
每次测试方法执行之前,都会执行 setUp 方法,此方法用于进行一些固定的准备工作,比如,实例化对象,打开网络连接等。 - tearDown
每次测试方法执行之后,都会执行 tearDown 方法,此方法用于进行一些固定的善后工作,比如,关闭网络连接等。
Junit3.x与4.x区别
Failure与Error区别
单元测试的失败(Failure)与测试出现了错误(Error)
JUnit 将测试失败的情况分为两种:Failure 和 Error 。 Failure 一般是由单元测试使用的断言方法判断失败引起的,它表示在测试点发现了问题(程序中的 bug);而 Error 则是有代码异常引起的,这是测试目的之外的发现,它可能产生于测试代码本身的错误(也就是说,编写的测试代码有问题),也可能是被测试代码中的一个隐藏 bug 。不过,一般情况下是第一种情况。
常用注解
- @Before
初始化方法,在任何一个测试方法执行之前,必须执行的代码。对比 JUnit 3 ,和 setUp()方法具有相同的功能。在该注解的方法中,可以进行一些准备工作,比如初始化对象,打开网络连接等。 - @After
释放资源,在任何一个测试方法执行之后,需要进行的收尾工作。对比 JUnit 3 ,和 tearDown()方法具有相同的功能。 - @Test
测试方法,表明这是一个测试方法。在 JUnit 中将会自动被执行。对与方法的声明也有如下要求:名字可以随便取,没有任何限制,但是返回值必须为 void ,而且不能有任何参数。如果违反这些规定,会在运行时抛出一个异常。不过,为了培养一个好的编程习惯,我们一般在测试的方法名上加 test ,比如:testAdd()。
同时,该 Annotation(@Test) 还可以测试期望异常和超时时间,如 @Test(timeout=100),我们给测试函数设定一个执行时间,超过这个时间(100毫秒),他们就会被系统强行终止,并且系统还会向你汇报该函数结束的原因是因为超时,这样你就可以发现这些 bug 了。而且,它还可以测试期望的异常,例如,我们刚刚的那个空指针异常就可以这样:@Test(expected=NullPointerException.class),此时如果出现空指针异常,反正会认为测试通过
- @Ignore
忽略的测试方法,标注的含义就是“某些方法尚未完成,咱不参与此次测试”;这样的话测试结果就会提示你有几个测试被忽略,而不是失败。一旦你完成了相应的函数,只需要把 @Ignore 注解删除即可,就可以进行正常测试了。 - @BeforeClass
针对所有测试,也就是整个测试类中,在所有测试方法执行前,都会先执行由它注解的方法,而且只执行一次。当然,需要注意的是,修饰符必须是 public static void xxxx ;此 Annotation 是 JUnit 4 新增的功能。 - @AfterClass
针对所有测试,也就是整个测试类中,在所有测试方法都执行完之后,才会执行由它注解的方法,而且只执行一次。当然,需要注意的是,修饰符也必须是 public static void xxxx ;此 Annotation 也是 JUnit 4 新增的功能,与 @BeforeClass 是一对。
执行顺序
所以,在 JUnit 4 中,单元测试用例的执行顺序为:
每一个测试方法的调用顺序为:
规范
- 单元测试代码应位于单独的 Source Folder 下
此 Source Folder 通常为 test ,这样可以方便的管理业务代码与测试代码。 - 测试类应该与被测试类位于同一 package名 下
- 选择有意义的测试方法名
无论是 JUnit 4 ,还是 JUnit 3 ,单元测试方法名均需使用 test<待测试方法名称>[概要描述] - 保存测试的独立性
每项单元测试都必须独立于其他所有单元测试而运行,因为单元测试需能以任何顺序运行。 - 为暂时未实现的测试代码忽略(@Ignore)或抛出失败(fail)
在 JUnit 4 中,可以在测试方法上使用注解 @Ignore 。在 JUnit 3 中,可以在未实现的测试方法中使用 fail(“测试方法未实现”); 以告知失败是因为测试方法未实现。 - 在调用断言(assert)方法时给出失败的原因
在使用断言方法时,请使用带有 message 参数的 API ,并在调用时给出失败时的原因描述,如 assertNotNull(”对象为空”, new Object())。
运行器Runner
类注解@RunWith,指定不同的类可以改变测试类的行为
参数化测试
主要是针对一些相同功能却要进行多组参数测试的情况,开发步骤如下:
- 参数化测试的类和普通测试类不同,要用特殊的Runner,类注解需要改为@RunWith(Parameterized.class)
- 定义该测试类的成员变量,一个放测试参数,另一个放该参数产生的预期结果
- 定义测试数据集合方法 public static Collection data() {…},注意该方法要用@Parameters修饰(数据格式为二维数组)
定义带参数的构造函数,注意定义数据集合时,要和构造函数参数次序一致
//指定@RunWith @RunWith(Parameterized.class) public class ParamTest { //定义成员变量,i为测试参数,j为测试结果 private int i; private int j; //构造函数 public ParamTest(int i, int j) { super(); this.i = i; this.j = j; } //测试数据集合,注意使用的注解,数据结构及次序 @Parameters public static Collection data() { return Arrays.asList(new Object[][]{{1,2},{3,4},{4,6}}); } @Test public void testMethod1() { System.out.println(i); System.out.println(j); //简单测试,只测试参数加1会不会等于预期结果 Assert.assertEquals(i+1, j); } }
Mock/Stub
如果你真的去把所有获取测试你的服务的前置条件的代码都完成,然后再写你的单元测试代码的话,这就不是单元测试了,这叫自动化测试,所以我们需要mock。
Mockito是Google Code上的一个开源项目,只需要在“运行测试代码”之前对接口进行Stub,也即设置方法的返回值或抛出的异常,然后直接运行测试代码,运行期间调用Mock的方法,会返回预先设置的返回值或抛出异常,最后再对测试代码进行验证。
Mockito创建mock对象不能对final,Anonymous ,primitive类和静态方法进行mock, powermock通过修改字节码来支持了此功能。
验证调用行为
import static org.mockito.Mockito.*; //创建Mock List mockedList = mock(List.class); //使用Mock对象 mockedList.add("one"); mockedList.clear(); //验证行为 verify(mockedList).add("one"); verify(mockedList).clear();
对Mock对象进行Stub
//也可以Mock具体的类,而不仅仅是接口 LinkedList mockedList = mock(LinkedList.class); //Stub when(mockedList.get(0)).thenReturn("first"); // 设置返回值 when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出异常 //第一个会打印 "first" System.out.println(mockedList.get(0)); //接下来会抛出runtime异常 System.out.println(mockedList.get(1)); //接下来会打印"null",这是因为没有stub get(999) System.out.println(mockedList.get(999)); // 可以选择性地验证行为,比如只关心是否调用过get(0),而不关心是否调用过get(1) verify(mockedList).get(0);
注意:android SDK中的库,大部分都只定义了接口而在jvm直接运行时是没有实现的,需要在虚拟机或者真机上运行,所以尽量使用自己导入的库,因为这些库带有具体实现,测试方法才能有效。
单元测试评估
单元测试任务
- 接口功能测试:用来保证接口功能的正确性。
- 局部数据结构测试(不常用):用来保证接口中的数据结构是正确的
- 比如变量有无初始值
- 变量是否溢出
- 边界条件测试
- 变量没有赋值(即为NULL)
- 变量是数值(或字符)
- 主要边界:最小值,最大值,无穷大(对于DOUBLE等)
- 溢出边界(期望异常或拒绝服务):最小值-1,最大值+1
- 临近边界:最小值+1,最大值-1
- 变量是字符串
- 引用“字符变量”的边界
- 空字符串
- 对字符串长度应用“数值变量”的边界
- 变量是集合
- 空集合
- 对集合的大小应用“数值变量”的边界
- 调整次序:升序、降序
- 变量有规律
- 比如对于Math.sqrt,给出n^2-1,和n^2+1的边界
- 所有独立执行通路测试:保证每一条代码,每个分支都经过测试
- 代码覆盖率
- 语句覆盖:保证每一个语句都执行到了
- 判定覆盖(分支覆盖):保证每一个分支都执行到
- 条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
- 路径覆盖:保证每一个路径都覆盖到
- 相关软件
- Cobertura:语句覆盖
- Emma: Eclipse插件Eclemma
- 代码覆盖率
- 各条错误处理通路测试:保证每一个异常都经过测试
代码覆盖率
在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到80%或 90%。于是乎,测试人员费尽心思设计案例覆盖代码。用代码覆盖率来衡量,有利也有有弊。
代码覆盖率 = 代码的覆盖程度,一种度量方式。
语句覆盖(StatementCoverage)
又称行覆盖(LineCoverage),段覆盖(SegmentCoverage),基本块覆盖(BasicBlockCoverage),这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了。 需要注意的是,单独一行的花括号{} 也常常被统计进去。 语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中的执行语句,却不考虑各种分支的组合等等。 假如你的上司只要求你达到语句覆盖,那么你可以省下很多功夫,但是,换来的确实测试效果的不明显,很难更多地发现代码中的问题。
这里举一个不能再简单的例子,我们看下面的被测试代码:
int foo(int a, int b)
{
return a / b;
}
假如我们的测试人员编写如下测试案例:
TeseCase: a = 10, b = 5
以上代码当b=0时程序异常,但是语句覆盖率为100%
判定覆盖(DecisionCoverage)
又称分支覆盖(BranchCoverage),所有边界覆盖(All-EdgesCoverage),基本路径覆盖(BasicPathCoverage),判定路径覆盖(Decision-Decision-Path)。它度量程序中每一个判定的分支是否都被测试到了。 这句话是需要进一步理解的,应该非常容易和下面说到的条件覆盖混淆。因此我们直接介绍第三种覆盖方式,然后和判定覆盖一起来对比,就明白两者是怎么回事了。
条件覆盖(ConditionCoverage)它度量判定中的每个子表达式结果true和false是否被测试到了。为了说明判定覆盖和条件覆盖的区别,我们来举一个例子,假如我们的被测代码如下:
int foo(int a, int b)
{
if (a < 10 || b < 10) // 判定
{
return 0; // 分支一
}
else
{
return 1; // 分支二
}
}
设计判定覆盖案例时,我们只需要考虑判定结果为true和false两种情况,因此,我们设计如下的案例就能达到判定覆盖率100%:
TestCaes1: a = 5, b = 任意数字 覆盖了分支一
TestCaes2: a = 15, b = 15 覆盖了分支二
设计条件覆盖案例时,我们需要考虑判定中的每个条件表达式结果,为了覆盖率达到100%,我们设计了如下的案例:
TestCase1: a = 5, b = 5 true, true
TestCase4: a = 15, b = 15 false, false
通过上面的例子,我们应该很清楚了判定覆盖和条件覆盖的区别。需要特别注意的是:条件覆盖不是将判定中的每个条件表达式的结果进行排列组合,而是只要每个条件表达式的结果true和false测试到了就OK了。因此,我们可以这样推论:完全的条件覆盖并不能保证完全的判定覆盖。比如上面的例子,假如我设计的案例为:
TestCase1: a = 5, b = 15 true, false 分支一
TestCase1: a = 15, b = 5 false, true 分支一
我们看到,虽然我们完整的做到了条件覆盖,但是我们却没有做到完整的判定覆盖,我们只覆盖了分支一。
路径覆盖(PathCoverage)
又称断言覆盖(PredicateCoverage)。它度量了是否函数的每一个分支都被执行了,测试函数中所有可能的路径。 这句话也非常好理解,就是所有可能的分支都执行一遍,有多个分支嵌套时,需要对多个分支进行排列组合,可想而知,测试路径随着分支的数量指数级别增加。比如下面的测试代码中有两个判定分支:
int foo(int a, int b)
{
int nReturn = 0;
if (a < 10)
{// 分支一
nReturn += 1;
}
if (b < 10)
{// 分支二
nReturn += 10;
}
return nReturn;
}
对上面的代码,我们分别针对我们前三种覆盖方式来设计测试案例:
语句覆盖
TestCase a = 5, b = 5 nReturn = 11
语句覆盖率100%判定覆盖
TestCase1 a = 5, b = 5 nReturn = 11 TestCase2 a = 15, b = 15 nReturn = 0
判定覆盖率100%条件覆盖
TestCase1 a = 5, b = 15 nReturn = 1 TestCase2 a = 15, b = 5 nReturn = 10
条件覆盖率100%
我们看到,上面三种覆盖率结果看起来都很酷!都达到了100%!主管可能会非常的开心,但是,让我们再去仔细的看看,上面被测代码中,nReturn的结果一共有四种可能的返回值:0,1,10,11,而我们上面的针对每种覆盖率设计的测试案例只覆盖了部分返回值,因此,可以说使用上面任一覆盖方式,虽然覆盖率达到了100%,但是并没有测试完全。接下来我们来看看针对路径覆盖设计出来的测试案例:
TestCase1 a = 5, b = 5 nReturn = 0
TestCase2 a = 15, b = 5 nReturn = 1
TestCase3 a = 5, b = 15 nReturn = 10
TestCase4 a = 15, b = 15 nReturn = 11
</code>路径覆盖率100%
太棒了!路径覆盖将所有可能的返回值都测试到了。这也正是它被很多人认为是“最强的覆盖”的原因了。
还有一些其他的覆盖方式,如:循环覆盖(LoopCoverage),它度量是否对循环体执行了零次,一次和多余一次循环。剩下一些其他覆盖方式就不介绍了。
Jacoco
Jacoco可以嵌入到Ant、Maven中,也可以使用Java Agent技术监控任意Java程序,也可以使用Java Api来定制功能。
Jacoco会监控JVM中的调用,生成监控结果(默认保存在jacoco.exec文件中),然后分析此结果,配合源代码生成覆盖率报告。需要注意的是:监控和分析这两步,必须使用相同的Class文件,否则由于Class不同,而无法定位到具体的方法,导致覆盖率均为0%。
Java Agent嵌入
首先,需要下载jacocoagent.jar文件,然后在Java程序启动参数后面加上 -javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2],具体的options可以在此页面找到。默认会在JVM关闭时(注意不能是kill -9),输出监控结果到jacoco.exec文件中,也可以通过socket来实时地输出监控报告(可以在Example代码中找到简单实现)。
Java Report
可以使用Ant、Mvn或Eclipse来分析jacoco.exec文件,也可以通过API来分析。
public void createReport() throws Exception {
// 读取监控结果
final FileInputStream fis = new FileInputStream(new File("jacoco.exec"));
final ExecutionDataReader executionDataReader = new ExecutionDataReader(fis);
// 执行数据信息
ExecutionDataStore executionDataStore = new ExecutionDataStore();
// 会话信息
SessionInfoStore sessionInfoStore = new SessionInfoStore();
executionDataReader.setExecutionDataVisitor(executionDataStore);
executionDataReader.setSessionInfoVisitor(sessionInfoStore);
while (executionDataReader.read()) {}
fis.close();
// 分析结构
final CoverageBuilder coverageBuilder = new CoverageBuilder();
final Analyzer analyzer = new Analyzer(executionDataStore, coverageBuilder);
// 传入监控时的Class文件目录,注意必须与监控时的一样
File classesDirectory = new File("classes");
analyzer.analyzeAll(classesDirectory);
IBundleCoverage bundleCoverage = coverageBuilder.getBundle("Title");
// 输出报告
File reportDirectory = new File("report"); // 报告所在的目录
final HTMLFormatter htmlFormatter = new HTMLFormatter(); // HTML格式
final IReportVisitor visitor = htmlFormatter.createVisitor(new FileMultiReportOutput(reportDirectory));
// 必须先调用visitInfo
visitor.visitInfo(sessionInfoStore.getInfos(), executionDataStore.getContents());
File sourceDirectory = new File("src"); // 源代码目录
// 遍历所有的源代码
// 如果不执行此过程,则在报告中只能看到方法名,但是无法查看具体的覆盖(因为没有源代码页面)
visitor.visitBundle(bundleCoverage, new DirectorySourceFileLocator(sourceDirectory, "utf-8", 4));
// 执行完毕
visitor.visitEnd();
}
</code>Android testDebug任务找不到的原因http://stackoverflow.com/questions/31937815/task-with-name-testdebug-not-found-in-project-module
其他(Android)
test与androidTest的区别
- test为单元测试,androidTest为与界面有关的测试,包括自动化测试
- gradle项目依赖,testCompile与androidTestCompile
- androidTest需要在模拟器或者真机上运行
http://stackoverflow.com/questions/29021331/confused-about-testcompile-and-androidtestcompile-in-android-gradle
Studio单元测试步骤
创建项目——根据需要testCompile或者androidTestCompile相应的测试支持库——设置Build Variant为相应的测试环境——Edit Configuration ——测试驱动开发,可以先写空实现的接口,通过IDE的快捷操作方式自动生成相应目录下的测试类——运行相应的TestCase
常见错误
- No tests found in …
如果使用安卓自身的Instrumentation相关的测试,studio自动生成的TestCase不会去继承TestCase,是不会被Runner所判断为测试用例的,所以自然找不到测试,根据需要自己选择要继承的基类
单元测试不需要使用Instrumentation,
This is where you need to select “Build Variants” on the left of Android Studio and make sure “Unit Tests” is selected
一定要选择这个选项,否则所有的步骤都是以Instrumentation框架进行,大多用于自动化测试