单元测试
单元测试是代码正确性验证的最重要的工具,也是系统测试当中最重要的环节。在标准的开发过程中,单元测试的代码与实际程序的代码具有同等的重要性。
好处
1、单元测试在实际开发中代替main方法验证内部类的正确性。
2、提高代码质量和可维护性,保证模块修改后的兼容性。可以通过单元测试来判断一个模块在修改后是否与修改前保持兼容、多大程度上不兼容、影响范围有多大。
JUnit5
与以前版本的JUnit不同,JUnit 5由三个不同子项目中的几个不同模块组成。
- Platform:位于架构的最底层,是JVM上执行单元测试的基础平台,还对接了各种IDE(例如IDEA、eclipse)或构建工具,控制台进行单元测试,并且还与引擎层对接,定义了引擎层对接的API;
- Jupiter:位于引擎层,支持JUnit 5版本的编程模型、扩展模型;
- Vintage:位于引擎层,实现向后兼容性,执行低版本的测试用例,即JUnit 5可以使用Vintage库运行JUnit 4测试,兼容旧版本单元测试。
优势
-
提供全新的断言和测试注解,支持测试类内嵌
-
更丰富的测试方式:支持动态测试,重复测试,参数化测试等
-
实现了模块化,让测试执行和测试发现等不同模块解耦,减少依赖
-
提供对 Java 8 的支持,如 Lambda 表达式,Sream API等。
迁移指南
JUnit 平台可以通过 Jupiter 引擎来运行 JUnit 5 测试,Vintage 引擎来运行 JUnit 3 和 JUnit 4 测试。因此,已有的 JUnit 3 和 4 的测试不需要任何修改就可以直接在 JUnit 平台上运行。只需要确保 Vintage 引擎的 jar 包出现在 classpath 中,JUnit 平台会自动发现并使用该引擎来运行 JUnit 3 和 4 测试。开发人员可以按照自己的项目安排来规划迁移到 JUnit 5 的进度。可以保持已有的 JUnit 3 和 4 的测试用例不变,而新增加的测试用例则使用 JUnit 5。
SpringBoot版本依赖
1、当前父pom中SpringBoot版本为2.3.8.RELEASE,
<parent>
<groupId>com.**.cloud</groupId>
<artifactId>**-starter-parent</artifactId>
<version>1.2.0</version>
</parent>
2、JUnit5的jar都被spring-boot-starter-test间接依赖进来。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
注意事项
- 一个service对应一个test;
- 一个方法对应多个test
- 正向测试
- 这种测试前期任务是要准备足够的正确数据(前提是要保证数据的正确性,这个很重要),运行代码后返回的值或产生的影响是要跟自己的预期是一致的!
- 异常输入
-
边界条件数据,比如值类型数据的最大值、最小值、DBNull,或者是方法中所使用的条件边界,例如a>100那么100就变成了这个数据的边界。而且在测试的时候还必须把超出边界的数据作为测试条件进行测试。
-
空数据,一般空数据对应于引用类型的数据,也就是Null值。
-
格式不正确数据,对于引用类型的数据或者结构对象,类型虽然正确但是其内部的数据结构不正确的数据。例如一个数据库实体对象,数据库中要求其某个属性必须为非空,但是这时我们可以设置为空进行测试。
-
- 正向测试
- 每个特性只测一次,在测试模式下,有时会情不自禁的滥用断言。这种做法会导致维护更困难,需要极力避免。仅对测试方法名指示的特性进行明确测试。对于一般性代码而言,保证测试代码尽可能少是一个重要目标。
- 使用显式断言,应该总是优先使用“assertEquals(a, b)”而不是“assertTrue(a == b)”,因为前者会给出更有意义的测试失败信息。在事先不确定输入值的情况下,这条规则尤为重要,比如之前使用随机参数值组合的例子。
测试方法命名
在单元测试中我们尽量少些注释,以至于不写,那么我们就要写出易读的测试名称,建议可以采取 “give操作对象+when测试对象+then断言” 的模式。
- 测试对象:将要测试的对象,即方法名称AddUser,DeleteUser等;
- 操作对象:将要对这个对象进行什么样的操作,比如有效的用户名,无效的用户名等;
- 断言:对结果做出判断,比如这个操作会抛异常,这个操作正常,这个操作会失败,这个值会发生改变等。
例:
当添加一个有效的用户的时候操作成功
addUser_ShouldSuccess_WhenGivenValidUserInfo()
当删除用户时该Id用户不存在抛出异常。
deleteById_ShouldThrows_WhenGivenIdNotExist
常用注解
ExtendWith:
在不同的Spring Boot版本中@ExtendWith的使用也有所不同,其中在Spring boot 2.1.x之前, @SpringBootTest 需要配合@ExtendWith(SpringExtension.class)才能正常工作的。
而在Spring boot 2.1.x之后,我们查看@SpringBootTest 的代码会发现,其中已经组合了@ExtendWith(SpringExtension.class),因此,无需在进行该注解的使用了。如果对扩展性有更多需求,可以添加@ExtendWith注解。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class})
public @interface SpringBootTest {
Mock
对函数的调用均执行mock(即虚假函数),不执行真正部分,默认返回null.
InjectMocks
执行注入mock对象的操作,可以调用真实代码的方法,在 setUp方法中 执行 MockitoAnnotations.initMocks(this); 的时候,会将标记了 @Mock 或 @Spy 的属性注入到 service 中。
Spy
Spy声明的对象,对函数的调用均执行真正部分。
Test:
注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。JUnit 5不再需要手动将测试类与测试方法为public,包可见的访问级别就足够了。
BeforeAll:
被该注解修饰的必须是静态方法,只执行一次,执行时机是在所有测试和 @BeforeEach 注解方法之前,会被子类继承,取代低版本的BeforeClass;
AfterAll:
被该注解修饰的必须是静态方法,执行时机是在所有测试和 @AfterEach 注解方法之后,会被子类继承,取代低版本的AfterClass;
BeforeEach:
被该注解修饰的方法会在每个测试方法执行前被执行一次,会被子类继承,取代低版本的Before;
AfterEach:
被该注解修饰的方法会在每个测试方法执行后被执行一次,会被子类继承,取代低版本的After;
DisplayName:
测试方法的展现名称,在测试框架中展示,支持emoji;
Timeout:
超时时长,被修饰的方法如果超时则会导致测试不通过;
Disabled:
不执行的测试方法;
Mockito框架
打桩测试
Mockito 中 when().thenReturn(); 这种语法来定义对象方法和参数(输入),然后在 thenReturn 中指定结果(输出)。此过程称为 Stub 打桩 。一旦这个方法被 stub 了,就会一直返回这个 stub 的值。
-
注意:
1、对于 static 和 final 方法, Mockito 无法对其 when(…).thenReturn(…) 操作。当我们连续两次为同一个方法使用 stub 的时候,只会只用最新的一次。
2、@Spy注解下,doReturn().when()是无副作用的,不会调用真实方法;when().thenReturn()是有副作用的,即会调用真实的方法,如果你不想调用真实的方法而是想要mock的话,建议使用doReturn().when()
verify
verify(class,time()).method();验证调用次数。
InOrder
调用顺序验证
InOrder inOrder=inOrder(mockClass);
inOrder.verify(mockClass).methd1();
inOrder.verify(mockClass).methd2();
断言
断言 (assertion) 是 org.junit.jupiter.api.Assertions 类上的众多静态方法之一。准备好测试实例、执行了被测类的方法以后,断言能确保你得到了想要的结果。一般的断言,无非是检查一个实例的属性(比如,判空与判非空等),或者对两个实例进行比较(比如,检查两个实例对象是否相等)等。无论哪种检查,断言方法都可以接受一个字符串作为最后一个可选参数,它会在断言失败时提供必要的描述信息。如果提供出错信息的过程比较复杂,它也可以被包装在一个 lambda 表达式中,这样,只有到真正失败的时候,消息才会真正被构造出来。
断言用于测试一个条件,该条件必须计算为 true ,测试才能继续执行。如果断言失败,测试会在断言所在的代码行上停止,并生成断言失败报告。如果断言成功,测试会继续执行下一行代码。
Assertions.assertEquals(expected,actual);
: 如果expected和actual不相等,断言失败;
Assertions.assertFalse(condition)
:如果condition不是false,断言失败;
Assertions.assertTrue(condition)
:如果condition不是true,断言失败;
Assertions.assertNull(actual)
:如果actual不是null,断言失败;
Assertions.assertNotNull(actual)
:如果actual是null,断言失败;
异常断言
JUnit5提供了一种新的断言方式Assertions.assertThrows(),配合函数式编程就可以进行使用。
示例代码
IntelliJ IDEA中用快捷键自动创建测试类的默认按键为:
ctrl+shift+t --> create new test–>勾选需要测试的方法
@Test
void deleteById_ShouldThrows2_WhenGivenStatusOnId() {
ProgramEntity entity = new ProgramEntity();
entity.setMiniProgramStatus(ProgramStatusEnum.ON.getValue());
doReturn(entity).when(programMapper).selectById(1);
Assertions.assertThrows(ReachBusinessException.class, () -> programService.deleteById(1),"小程序已上架不允许删除!");
}
超时测试
当被测试的程序,逻辑非常复杂,不清楚是否存在问题,也要考虑这个程序的效率,在一定的时间内要执行完,这种情况要加一个timeout进行限制,当程序超过一定的时间没有执行完,就认为它是错误的。
参数化测试
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
@DisplayName("程序上架成功")
public void programUp_ShouldSuccess_WhenGivenRightParam(Integer programId) {
doReturn(entity1).when(programMapper).selectById(miniProgramId);
ProgramEntity updateEntity = new ProgramEntity();
updateEntity.setMiniProgramStatus(ProgramStatusEnum.ON.getValue());
updateEntity.setUpdateUser(OperationLogUtils.loginUserName());
updateEntity.setId(entity1.getId());
doReturn(1).when(ProgramMapper).updateById(updateEntity);
programService.miniProgramUp(programId);
}