单元测试就是针对最小的功能单元编写测试代码。
Java程序最小的功能单元是方法,因此,对Java程序进行单元测试就是针对单个Java方法的测试。

测试驱动开发——所谓测试驱动开发,是指先编写接口中,紧接着编写测试。编写完测试后, 我们才开始真正编写实现代码。在编写实现代码的过程中,一边写,一边测,什么时候测试全部通过了,那就表示编写的实现完成了:
编写JUnit测试 - 图1
这就是传说中的 TDD(Test-Driven Development)
当然,这是一种理想情况。大部分情况是我们已经编写了实现代码,需要对已有的代码进行测试。
我们先通过一个示例来看如何编写测试。
假定我们编写了一个计算阶乘的类,它只有一个静态方法来计算阶乘

  1. public class Factorial {
  2. public static long fact(long n){
  3. long r = 1;
  4. for(int i=1;i<=n;i++){
  5. r*=i;
  6. }
  7. return r;
  8. }
  9. }

要测试这个方法,一个很自然的想法是编写一个main()方法,然后运行一些测试代码

public class Main {
    public static void main(String[] args) {

        fact(10) == 3628800 ? System.out.println("pass") : System.out.println("fail");
    }
}

这种通过运行main()进行测试的方式有很多缺点

  • 只能有一个main() 方法,不能把测试代码分享
  • 没有打印出测试结果和期望结果
  • 很难编写一组通用的测试代码

JUnit

JUnit是一个开源的Java语言的单元测试框架,专门针对Java设计,使用最广泛。JUnit是事实上的单元测试的标准框架,任何Java开发者都应当学习并使用JUnit编写单元测试。
使用JUnit的好处在于,我们可以感觉简单地组织测试代码,并随时运行它们,JUnit就会给出成功的测试和失败的测试,还可以生成测试报告,不仅包含测试的成功率,还可以统计测试的代码覆盖率,即被测试的代码本身有多少经过了测试。对于高质量的代码来说,测试覆盖率应该在80%以上。
此外,几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写并运行JUnit测试。

以Eclipse为例,当我们已经编写了一个Factoryial.java文件后,我们想对其进行测试,需要编写一个对应的FactorialTest.java文件,以Test为后缀是一个惯例,并分别将其放入srctest目录中,最后,在Project-Properties-Java Build Path-Libraries中添加JUnit 5库。

手动创建栏目test需要选择source Folder

我们来看一下FactorialTest.java的内容

package com.itranswarp.learnjava;

import static org.junit.Assert.assertEqals;
import org.junit.jupiter.api.Test;

public class FactorialTest {
    @Test
    void TestFact(){
        assertEquals(1,Factorial.fact(1));
        assertEquals(2,Factorial.fact(2));
        assertEquals(24,Factorial.fact(4));
    }
}

核心测试方法testFact()加上了@Test注解,这是JUnit要求的,它会把带有@Test的方法识别为测试方法。
在测试方法内部,我们用assertEquals(1, Factorial.fact(1))表示,期望Factorial.fact(1)返回1
assertEquals(expected, actual)是最常用的测试方法,它在Assertion类中定义。Assertion还定义了其他断言方法,例如:

  • assertTrue():期待结果为true
  • assertFalse():期待结果为false
  • assertNotNull():期待结果为非null
  • assertArrayEquals():期待结果为数组并与期望数组每个元素的值均相等
  • ……

运行单元测试非常简单,选中FactorialTest.java文件,点击Run-Run as - JUnit Test
如果测试结果与预期不符,assertEquals()会抛出异常,我们就会得到 一个测试失败的结果,此时,我们要么修正实现代码,要么修正测试代码,直到测试通过为止。
使用浮点数时,由于浮点数无法精确地进行比较,因此,我们需要调用 assertEquals(double expected, double actual, double delta)这个重载方法,指定一个误差值。
assertEquals(0.1, Math.abs(1-9/10.0), 0.0000001);

单元测试的好处

单元测试可以确保单个方法按照正确地预期运行,如果修改了某个方法的代码,只需要确保其对应的单元测试通过,即可认为改动正确。
此外,测试代码本身就可以作为示例代码,用来演示如何调用该方法。
使用JUnit 进行单元测试,我们可以使用断言(Assertion) 来测试期望结果,可以方便地组织和运行测试,并方便地查看测试结果。
此外,JUnit既可以直接在IDE中运行,也可以方便地集成到Maven这些自动化工具中运行。
在编写单元测试的时候,我们一定要遵循一定的规范:

  1. 单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试
  2. 每个单元测试应当互相独立,不依赖运行的顺序
  3. 测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为0null,空字符串''等情况

一个JUnit测试包含若干@Test方法,并使用Assertions进行断言,注意浮点数assertEquals()要指定delta。