关于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,指定不同的类可以改变测试类的行为

参数化测试

主要是针对一些相同功能却要进行多组参数测试的情况,开发步骤如下:

  1. 参数化测试的类和普通测试类不同,要用特殊的Runner,类注解需要改为@RunWith(Parameterized.class)
  2. 定义该测试类的成员变量,一个放测试参数,另一个放该参数产生的预期结果
  3. 定义测试数据集合方法 public static Collection data() {…},注意该方法要用@Parameters修饰(数据格式为二维数组)
  4. 定义带参数的构造函数,注意定义数据集合时,要和构造函数参数次序一致

    
    //指定@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直接运行时是没有实现的,需要在虚拟机或者真机上运行,所以尽量使用自己导入的库,因为这些库带有具体实现,测试方法才能有效。

单元测试评估

单元测试任务

  1. 接口功能测试:用来保证接口功能的正确性。
  2. 局部数据结构测试(不常用):用来保证接口中的数据结构是正确的
    1. 比如变量有无初始值
    2. 变量是否溢出
  3. 边界条件测试
    1. 变量没有赋值(即为NULL)
    2. 变量是数值(或字符)
      1. 主要边界:最小值,最大值,无穷大(对于DOUBLE等)
      2. 溢出边界(期望异常或拒绝服务):最小值-1,最大值+1
      3. 临近边界:最小值+1,最大值-1
    3. 变量是字符串
      1. 引用“字符变量”的边界
      2. 空字符串
      3. 对字符串长度应用“数值变量”的边界
    4. 变量是集合
      1. 空集合
      2. 对集合的大小应用“数值变量”的边界
      3. 调整次序:升序、降序
    5. 变量有规律
      1. 比如对于Math.sqrt,给出n^2-1,和n^2+1的边界
  4. 所有独立执行通路测试:保证每一条代码,每个分支都经过测试
    1. 代码覆盖率
      1. 语句覆盖:保证每一个语句都执行到了
      2. 判定覆盖(分支覆盖):保证每一个分支都执行到
      3. 条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
      4. 路径覆盖:保证每一个路径都覆盖到
    2. 相关软件
      1. Cobertura:语句覆盖
      2. Emma: Eclipse插件Eclemma
  5. 各条错误处理通路测试:保证每一个异常都经过测试

代码覆盖率

在做单元测试时,代码覆盖率常常被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况,比如,代码覆盖率必须达到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的区别

  1. test为单元测试,androidTest为与界面有关的测试,包括自动化测试
  2. gradle项目依赖,testCompile与androidTestCompile
  3. androidTest需要在模拟器或者真机上运行
    http://stackoverflow.com/questions/29021331/confused-about-testcompile-and-androidtestcompile-in-android-gradle

Studio单元测试步骤

创建项目——根据需要testCompile或者androidTestCompile相应的测试支持库——设置Build Variant为相应的测试环境——Edit Configuration ——测试驱动开发,可以先写空实现的接口,通过IDE的快捷操作方式自动生成相应目录下的测试类——运行相应的TestCase

常见错误

  1. 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框架进行,大多用于自动化测试

版权声明

![Creative Commons BY-NC-ND 4.0 International License](/images/cc.png)

Lam’s Blog by Binghe Lin is licensed under a Creative Commons BY-NC-ND 4.0 International License.
林炳河创作并维护的Lam’s Blog采用创作共用保留署名-非商业-禁止演绎4.0国际许可证

本文首发于Lam’s Blog - Knowledeg as Action,版权所有,侵权必究。

本文永久链接:http://linbinghe.com/2017/3698e116.html