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
为后缀是一个惯例,并分别将其放入src
和test
目录中。
在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)
返回1
。assertEquals(expected, actual)
是最常用的测试方法,它在Assertion
类中定义。Assertion
还定义了其他断言方法,例如:
assertTrue()
: 期待结果为true
assertFalse()
: 期待结果为false
assertNotNull()
: 期待结果为非null
assertArrayEquals()
: 期待结果为数组并与期望数组每个元素的值均相等- ...
单元测试的好处
单元测试可以确保单个方法按照正确预期运行,如果修改了某个方法的代码,只需确保其对应的单元测试通过,即可认为改动正确。此外,测试代码本身就可以作为示例代码,用来演示如何调用该方法。
使用JUnit进行单元测试,我们可以使用断言(Assertion
)来测试期望结果,可以方便地组织和运行测试,并方便地查看测试结果。此外,JUnit既可以直接在IDE中运行,也可以方便地集成到Maven这些自动化工具中运行。
在编写单元测试的时候,我们要遵循一定的规范:
一是单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试;
二是每个单元测试应当互相独立,不依赖运行的顺序;
三是测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为0
,null
,空字符串""
等情况。
使用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的套路如下:
- 对于实例变量,在
@BeforeEach
中初始化,在@AfterEach
中清理,它们在各个@Test
方法中互不影响,因为是不同的实例; - 对于静态变量,在
@BeforeAll
中初始化,在@AfterAll
中清理,它们在各个@Test
方法中均是唯一实例,会影响各个@Test
方法。
大多数情况下,使用@BeforeEach
和@AfterEach
就足够了。只有某些测试资源初始化耗费时间太长,以至于我们不得不尽量“复用”时才会用到@BeforeAll
和@AfterAll
。
最后,注意到每次运行一个@Test
方法前,JUnit首先创建一个XxxTest
实例,因此,每个@Test
方法内部的成员变量都是独立的,不能也无法把成员变量的状态从一个@Test
方法带到另一个@Test
方法。