Java单元测试
什么是单元测试?单元测试就是针对最小的功能单元编写测试代码。对于Java来说,最小的功能单元是方法,因此对Java单元测试就是对Java单个方法测试。
测试驱动开发:
是指先编写接口,接着写测试,再开始编写实现代码。在编写过程中,边写边测,测试通过之时就是代码大成之日。传说中的TDD。
当然测试驱动开发是一种理想状态。大部分情况是先写完了实现代码,需要测试…..
这时候就需要一种测试框架,辅助编写测试。
JUnit
JUnit是一个开源的Java语言的单元测试框架,专门针对Java设计,使用最广泛。JUnit是事实上的单元测试的标准框架,任何Java开发者都应当学习并使用JUnit编写单元测试。
好处:我们可以非常简单地组织测试代码,并随时运行它们,JUnit就会给出成功的测试和失败的测试,还可以生成测试报告,不仅包含测试的成功率,还可以统计测试的代码覆盖率,即被测试的代码本身有多少经过了测试。对于高质量的代码来说,测试覆盖率应该在80%以上。
几乎所有的IDE工具都集成了JUnit,这样我们就可以直接在IDE中编写并运行JUnit测试。
以Eclipse为例,当我们已经编写了一个Factorial.java
文件后,我们想对其进行测试,需要编写一个对应的FactorialTest.java
文件,以Test
为后缀是一个惯例,并分别将其放入src
和test
目录中。最后,在Project
- Properties
- Java Build Path
- Libraries
中添加JUnit 5
的库。
举个栗子
- 这个是需要被测试的code
1 | //Factorial.java |
- 再写个测试code,文件名一般以Test结尾
assertEquals(expected, actual)
是最常用的测试方法,它在Assertion
类中定义。Assertion
还定义了其他断言方法,例如:assertTrue()
: 期待结果为true
assertFalse()
: 期待结果为false
assertNotNull()
: 期待结果为非null
assertArrayEquals()
: 期待结果为数组并与期望数组每个元素的值均相等- …
1 | //FactorialTest.java |
- 运行:
- 结果
返回结果是没有运行通过测试,因为我们在测试代码中assertEquals期望执行计算阶乘3返回结果26
显然不对,那么就需要我们改一下,assertEquals(6, Factorial.fact(3));
,然后再运行测试代码
绿色的,非常OK。
注:使用浮点数时,由于浮点数无法精确地进行比较,因此,我们需要调用assertEquals(double expected, double actual, double delta)
这个重载方法,指定一个误差值:
1 | assertEquals(0.1, Math.abs(1 - 9 / 10.0), 0.0000001); |
在编写单元测试的时候,我们要遵循一定的规范:
一是单元测试代码本身必须非常简单,能一下看明白,决不能再为测试代码编写测试;
二是每个单元测试应当互相独立,不依赖运行的顺序;
三是测试时不但要覆盖常用测试用例,还要特别注意测试边界条件,例如输入为
0
,null
,空字符串""
等情况
使用Fixture
为了方便管理(创建、清除)每次@Test
方法调用的对象,JUnit提供了编写测试前准备,测试后清理的固定代码,我们称之为Fixture。
1 | public class CalculatorTest{ |
1 | //上面的测试代码在JUnit中运行顺序如下: |
还有一些资源初始化和清理可能更加繁琐,而且会耗费较长的时间,例如初始化数据库。JUnit还提供了@BeforeAll
和@AfterAll
,它们在运行所有@Test前后运行。因此,它们只能初始化静态变量。
因此,我们总结出编写Fixture的套路如下:
- 对于实例变量,在
@BeforeEach
中初始化,在@AfterEach
中清理,它们在各个@Test
方法中互不影响,因为是不同的实例; - 对于静态变量,在
@BeforeAll
中初始化,在@AfterAll
中清理,它们在各个@Test
方法中均是唯一实例,会影响各个@Test
方法。
异常测试
有时候我们需要对业务代码中,预期出现的异常做测试。举个栗子
1 | public class Factorial { |
现在对异常进行测试
1 |
|
JUnit提供assertThrows()
来期望捕获一个指定的异常。第二个参数Executable
封装了我们要执行的会产生异常的代码。当我们执行Factorial.fact(-1)
时,必定抛出IllegalArgumentException
。assertThrows()
在捕获到指定异常时表示通过测试,未捕获到异常,或者捕获到的异常类型不对,均表示测试失败。
有些童鞋会觉得编写一个Executable
的匿名类太繁琐了。实际上,Java 8开始引入了函数式编程,所有单方法接口都可以简写如下:
1 |
|
条件测试
在运行测试期间,有时候,需要根据执行环境不同,选择性执行测试用例。比如Win和Unix系统的一些路径不同,所以要根据不同的系统执行不同的测试。
1 |
|
@EnabledOnOs
就是一个条件判断
我们来看一些常用的条件测试:
- 不在Windows平台执行的测试,可以加上
@DisabledOnOs(OS.WINDOWS)
- 需要排出某些
@Test
方法,不要让它运行,这时,我们就可以给它标记一个@Disabled
- 只能在Java 9或更高版本执行的测试,可以加上
@DisabledOnJre(JRE.JAVA_8)
- 只能在64位操作系统上执行的测试,可以用
@EnabledIfSystemProperty
判断 - 需要传入环境变量
DEBUG=true
才能执行的测试,可以用@EnabledIfEnvironmentVariable
- 最后,万能的
@EnableIf
可以执行任意Java语句并根据返回的boolean
决定是否执行测试
1 | //演示了一个只能在星期日执行的测试 |
没有执行的测试,在运行测试结果是被标记为Skipped
参数化测试
如果待测试的输入和输出是一组数据: 可以把测试数据组织起来 用不同的测试数据调用相同的测试方法
参数化测试和普通测试稍微不同的地方在于,一个测试方法需要接收至少一个参数,然后,传入一组参数反复运行。
JUnit提供了一个@ParameterizedTest
注解,用来进行参数化测试。
假设我们想对Math.abs()
进行测试:
1 | //先用一组正数进行测试 |
注意到参数化测试的注解是@ParameterizedTest
,而不是普通的@Test
。
输入,预期输出都为参数
要用参数化测试的方法来测试,我们不但要给出输入,还要给出预期输出。因此,测试方法至少需要接收两个参数:
1 |
|
现在问题来了:参数如何传入?
- 最简单的方法是通过
@MethodSource
注解,它允许我们编写一个同名的静态方法来提供测试参数:
- 最简单的方法是通过
1 |
|
- 使用
@CsvSource
,它的每一个字符串表示一行,一行包含的若干参数用,
分隔
- 使用
1 |
|
- 如果有成百上千的测试输入,可以把测试数据提到一个独立的CSV文件中,然后标注上
@CsvFileSource
- 如果有成百上千的测试输入,可以把测试数据提到一个独立的CSV文件中,然后标注上
1 |
|
JUnit只在classpath中查找指定的CSV文件,因此,test-capitalize.csv
这个文件要放到test
目录下。