JUnit初识


简单的JUnit例子

先通过一个示例来看如何编写测试。假定我们编写了一个计算阶乘的类,它只有一个静态方法来计算阶乘:

n!=1×2×3×...×n

代码如下:

public class Factorial {
    public static long fact(long n) {
        long r = 1;
        for (long i = 1; i <= n; i++) {
            r = r * i;
        }
        return r;
    }
}

最自然的思路是创建一个main方法进行测试。

不过,使用main()方法测试有很多缺点:

一是只能有一个main()方法,不能把测试代码分离,二是没有打印出测试结果和期望结果,例如,expected: 3628800, but actual: 123456,三是很难编写一组通用的测试代码。

在此使用JUnit来编写单元测试,当我们已经编写了一个Factorial.java文件后,我们想对其进行测试,需要编写一个对应的FactorialTest.java文件,以Test为后缀是一个惯例,并分别将其放入srctest目录中。

在ideal中需要为Test文件夹Build path为Test Source Folders。

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

package com.tangzz.learnjava;

import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

public class FactorialTest {

    @Test
    void testFact() {
        assertEquals(1, Factorial.fact(1));
        assertEquals(2, Factorial.fact(2));
        assertEquals(6, Factorial.fact(3));
        assertEquals(3628800, Factorial.fact(10));
        assertEquals(2432902008176640000L, Factorial.fact(20));
    }
}

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

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

单元测试的好处

单元测试可以确保单个方法按照正确预期运行,如果修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动正确。此外,测试代码本身就可以作为示例代码,用来演示如何调用该方法。

使用JUnit进行单元测试,我们可以使用断言(Assertion)来测试期望结果,可以方便地组织和运行测试,并方便地查看测试结果。此外,JUnit既可以直接在IDE中运行,也可以方便地集成到Maven这些自动化工具中运行。

在编写单元测试的时候,我们要遵循一定的规范:

一是单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试;

二是每个单元测试应当互相独立,不依赖运行的顺序;

三是测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为0null,空字符串""等情况。

使用Fixture

在一个单元测试中,我们经常编写多个@Test方法,来分组、分类对目标代码进行测试。

在测试的时候,我们经常遇到一个对象需要初始化,测试完可能还需要清理的情况。如果每个@Test方法都写一遍这样的重复代码,显然比较麻烦。

JUnit提供了编写测试前准备、测试后清理的固定代码,我们称之为Fixture。

一个具体的Calculator的例子:

public class Calculator {
    private long n = 0;

    public long add(long x) {
        n = n + x;
        return n;
    }

    public long sub(long x) {
        n = n - x;
        return n;
    }
}

这个类的功能很简单,但是测试的时候,我们要先初始化对象,我们不必在每个测试方法中都写上初始化代码,而是通过@BeforeEach来初始化,通过@AfterEach来清理资源:

public class CalculatorTest {

    Calculator calculator;

    @BeforeEach
    public void setUp() {
        this.calculator = new Calculator();
    }

    @AfterEach
    public void tearDown() {
        this.calculator = null;
    }

    @Test
    void testAdd() {
        assertEquals(100, this.calculator.add(100));
        assertEquals(150, this.calculator.add(50));
        assertEquals(130, this.calculator.add(-20));
    }

    @Test
    void testSub() {
        assertEquals(-100, this.calculator.sub(100));
        assertEquals(-150, this.calculator.sub(50));
        assertEquals(-130, this.calculator.sub(-20));
    }
}

CalculatorTest测试中,有两个标记为@BeforeEach@AfterEach的方法,它们会在运行每个@Test方法前后自动运行。

上面的测试代码在JUnit中运行顺序如下:

for (Method testMethod : findTestMethods(CalculatorTest.class)) {
    var test = new CalculatorTest(); // 创建Test实例
    invokeBeforeEach(test);
        invokeTestMethod(test, testMethod);
    invokeAfterEach(test);
}

可见,@BeforeEach@AfterEach会“环绕”在每个@Test方法前后。

还有一些资源初始化和清理可能更加繁琐,而且会耗费较长的时间,例如初始化数据库。JUnit还提供了@BeforeAll@AfterAll,它们在运行所有@Test前后运行,顺序如下:

invokeBeforeAll(CalculatorTest.class);
for (Method testMethod : findTestMethods(CalculatorTest.class)) {
    var test = new CalculatorTest(); // 创建Test实例
    invokeBeforeEach(test);
        invokeTestMethod(test, testMethod);
    invokeAfterEach(test);
}
invokeAfterAll(CalculatorTest.class);

因为@BeforeAll@AfterAll在所有@Test方法运行前后仅运行一次,因此,它们只能初始化静态变量,例如:

public class DatabaseTest {
    static Database db;

    @BeforeAll
    public static void initDatabase() {
        db = createDb(...);
    }
    
    @AfterAll
    public static void dropDatabase() {
        ...
    }
}

事实上,@BeforeAll@AfterAll也只能标注在静态方法上。

因此,我们总结出编写Fixture的套路如下:

  1. 对于实例变量,在@BeforeEach中初始化,在@AfterEach中清理,它们在各个@Test方法中互不影响,因为是不同的实例;
  2. 对于静态变量,在@BeforeAll中初始化,在@AfterAll中清理,它们在各个@Test方法中均是唯一实例,会影响各个@Test方法。

大多数情况下,使用@BeforeEach@AfterEach就足够了。只有某些测试资源初始化耗费时间太长,以至于我们不得不尽量“复用”时才会用到@BeforeAll@AfterAll

最后,注意到每次运行一个@Test方法前,JUnit首先创建一个XxxTest实例,因此,每个@Test方法内部的成员变量都是独立的,不能也无法把成员变量的状态从一个@Test方法带到另一个@Test方法。