Junit5单元测试

之前已经学习过,测试方法大致可分为黑盒测试和白盒测试。Junit是一种白盒测试的工具

步骤

  1. 定义一个测试类:

    • 类名:被测试类名Test
    • 包名:xxx.xxx.xxx.test
  2. 定义测试方法:可以独立运行

    • 方法名:test测试方法名
    • 返回值:void
    • 参数列表:空参
    • 结果判断:Assertions.assertEquals(expected, result)

  3. 给方法加@Test注解, 这是对简单程序而言。

    如果是复杂项目如SpringBoot,就要用@SpringBootTest 。因为对象之间是有依赖的,直接new当前测试对象可能某些组成部分为null 。然后用@Autowired 将Bean注入;当没有真实数据时,可以用@MockBean 注解来模拟真实Bean, 此时需要相应的规则 , 如 when(obj.func()).thenReturn();但是如果每次都要配置规则其实很麻烦,于是还可以用@SpyBean 注解,有规则按规则,无规则按默认

  4. 导入Junit依赖 import org.junit.jupiter.api.Test; 最好选择Junit5

    还可以用JunitGenerator V2.0插件,在设置中进行配置,不过可选的只有Junit3和Junit4, 不建议使用

更简单的方法是直接用IDEA中内置的Junit模块,方法名上右键GOTO->Test

按需选择内容

复用

不同的模块之间,有些操作其实是一样的,因此就可以把一些复用的操作分离出来,如初始化资源申请等

@Before 注解

init()初始化方法,用于资源申请,所有测试方法执行之前

@BeforeEach 每个测试都执行一次

@BeforeAll 所有测试只执行一次,所以必须是static

@After 注解

close()释放资源方法,在所有测试方法执行完后,都会自动执行该方法,即使结果出错了

两种用法同上。

异常测试

测试时有时对可能发生的每种类型的异常进行测试

使用@Test注解

使用@Test注解自带的 expected = Exception.class 属性来断言需要抛出一个异常,如下:

1
2
3
4
@Test(expected = IllegalArgumentException.class)  
public void testExpectedException2() {
new Person('Joe', -1);
}

在运行测试的时候,此方法必须抛出异常,这个测试才算通过。

这种方法不能指定断言的异常信息,而且有一个潜在的问题:当被标记的这个测试方法中的任何一个操作抛出了相应的异常时,这个测试就会通过。这就意味着有可能抛出异常的地方并不是我们期望的那个操作。

使用try-catch

这种写法看上去和实现类的写法很相似,当没有异常被抛出的时候fail方法会被调用,输出测试失败的信息。

1
2
3
4
5
6
7
8
9
@Test  
public void testExpectedException3() {
try {
new Person('Joe', -1);
fail('Should have thrown an IllegalArgumentException because age is invalid!');
} catch (IllegalArgumentException e) {
assertThat(e.getMessage(), containsString('Invalid age'));
}
}

使用ExpectedException

这种方法除了可以指定期望抛出的异常类型之外还可以指定在抛出异常时希望同时给出的异常信息。它需要在测试之前使用Rule标记来指定一个ExpectedException,并在测试相应操作之前指定期望的Exception类型。

1
2
3
4
5
6
7
8
9
@Rule  
public ExpectedException exception = ExpectedException.none();

@Test
public void testExpectedException() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage(containsString('Invalid age'));
new Person('Joe', -1);
}

参数化测试

主要使用的是 Junit5提供的@ParameterizedTest

需要引入junit-jupiter-params,该包提供的注解类型有:

1
2
3
4
5
6
@ValueSource
@EnumSource
@MethodSource
@CsvSource
@CsvFileSource
@ArgumentsSource

value source是最简单的参数源,通过注解可以直接指定携带的运行参数。

  • String values: @ValueSource(strings = {“foo”, “bar”, “baz”})
  • Double values: @ValueSource(doubles = {1.5D, 2.2D, 3.0D})
  • Long values: @ValueSource(longs = {2L, 4L, 8L})
  • Integer values: @ValueSource(ints = {2, 4, 8})

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

public class ValueSourcesExampleTest {

@ParameterizedTest
@ValueSource(ints = {2, 4, 8})
void testNumberShouldBeEven(int num) {
assertEquals(0, num % 2);
}

@ParameterizedTest
@ValueSource(strings = {"Radar", "Rotor", "Tenet", "Madam", "Racecar"})
void testStringShouldBePalindrome(String word) {
assertEquals(isPalindrome(word), true);
}

@ParameterizedTest
@ValueSource(doubles = {2.D, 4.D, 8.D})
void testDoubleNumberBeEven(double num) {
assertEquals(0, num % 2);
}

boolean isPalindrome(String word) {
return word.toLowerCase().equals(new StringBuffer(word.toLowerCase()).reverse().toString());
}
}

其实批量输入最好是用CsvSource,使用示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TwoSumTest {
@ParameterizedTest
@CsvSource({
"'[1, 1]', 2, '[0, 1]'",
"'[3, 2, 3]', 6, '[0, 2]'",
"'[2, 7, 11, 15]', 9, '[0, 1]'"
})
void twoSum(
@ConvertWith(String2int.class) int[] nums,
int target,
@ConvertWith(String2int.class) int[] expect
) {
TwoSum solution = new TwoSum();
assertArrayEquals(expect, solution.twoSum(nums, target));
}
}

通过@CsvSource传入的每一项,就相当于一个csv文件的一行,是一个String。 每一行中,可以有若干列,这里是三列,代表测试函数的三个输入参数的一组值。 为了让值中包含分隔符,,需要用单引号''包含。

如果是基本数据类型,如intString之类,JUnit5会尝试直接转换。 而如果是不常见的数据类型,则需要使用@ConvertWith。 上一段代码中的@ConvertWith(String2int.class) int[] nums,,就是在用一个自定义类String2int.class,把String类型转换为int[]

CSVSource应用参考

JUnit 5 新特性

官方文档

单元测试理论

单元测试是针对最小的功能单元编写测试代码,对java来说,就是针对单个方法的测试。

TDD三条规则:

  • 除非为了使一个失败的单元测试通过,否则不允许编写任何产品代码
  • 在一个单元测试中只允许编写刚好能导致失败的内容(编译错误也算)
  • 只允许编写刚好能够使一个失败的单元测试通过的产品代码

对于一个软件,整体的测试过程是:单元测试 -> 集成测试 -> 系统测试 -> 性能测试

单元测试方法

分为代码级别测试和模块级别测试

代码级别测试:熟悉模块功能,内部逻辑与接口,编写测试用例

  1. 接口测试:确保接口实现符合文档规范
    • 调用本模块的输入参数是否正确(参数数目,属性,类型次序)
    • 是否修改了只读型参数
    • 全局量的定义在各模块是否一致
    • 在单元有多个入口的情况下,是否引用了与当前入口无关的参数
    • 常数是否当作变量来传递
  2. 数据结构测试:确保数据结构可用,例如数据库,文件,自定义的数据结构
    • 不正确或不一致的数据类型说明
    • 使用尚未赋值或尚未初始化的变量
    • 错误的初始值或错误的缺省值
    • 变量名拼写错
    • 上溢/下溢或地址异常
  3. 路径测试:尽可能对每一条独立执行路径进行测试,满足某种覆盖标准
    • 不同数据类型之间的比较
    • 错误的使用逻辑运算符或优先级
    • 关系表达式中比较运算出错
    • 循环终止条件或不可能出现
    • 迭代发散时不能退出
    • 错误的修改了循环变量
  4. 错误处理测试
  5. 边界测试:对于边界值进行测试
    • 普通合法数据是否正确处理
    • 普通非法数据是否正确处理
    • 边界内最接近边界的合法数据是否正确处理
    • 边界外最接近边界的非法数据是否正确处理
    • N次循环的第0,1,n次是否有错
    • 运算或判断中取最大最小值时是否有错
    • 数据流,控制流中刚好等于/大于/小于确定比较值时是否出错

模块级别测试:主要是功能测试,通过黑盒测试方法

其他测试项:性能,代码规范等