2651 字
13 分钟
测试实践:单元测试规范与质量门禁

单元测试#

单元测试(Java语言中)是对类中每个方法提供一个或多个测试的一种实践,其目的是为了有规律的测试一个类的各个部分是否具备正确的行为。 ——摘自《Java 编程思想》

引言#

刚开始工作的时候谁写测试类啊,还单元测试呢,测试我都不测试。后来到了正式一些的公司之后,不通过质量门禁则不能上线,所以 JaCoCo 这个开源的单元测试引擎就发挥作用了,它搭配 Sonar 面板可以展示单元测试对代码的覆盖程度、帮助开发团队分析测试覆盖率,并且了解哪些代码已经被测试覆盖,以及哪些代码尚未被测试覆盖。尽管如此,你的测试类还是很难保证你的代码所有逻辑分支都可以覆盖的到,于是 Mock 又出现了,相对于 Junit 它提供了简洁的API,使得在单元测试中创建模拟对象、定义模拟对象的行为以及验证方法的调用变得非常容易。所以我们选择使用 Mock 搭配 JUnit 作为最佳解决方案。

质量门襟要求#

除 web 语言之外的所有开发语言在投产时,增量代码单元测试行覆盖率要求 100%,如果在 80% 到 100% 范围,可以进行人工评审环节,评审过后方可通过质量门禁校验。并且单元测试成功率不得低于 100%。

规范的单元测试#

单元测试的目的是:提升软件质量、促进代码优化、提升研发效率、增加重构自信。单元测试需要符合两大类原则:FIRST 原则和 AIR 原则。

  • FITST原则:
    • 快速(Fast):单元测试能够快速执行。
    • 隔离(Isolated):单元测试不要依赖外部环境,如网络、第三方 Web Service 等。
    • 可重复(Repeatable):单元测试应该是可以被重复执行的,并且结果是相等的。
    • 自我验证(Self-verigying):单元测试应该是用例本身自动校验,不依赖人工验证。
    • 及时(Timely):单元测试应该及时进行编写、更新和维护,以保证测试用例可以随着业务代码的变化动态保证质量。
  • AIR 原则:
    • 自动化(Automatic)
    • 独立性(Independent)
    • 可重复(Repeatable)

屏蔽内容限制#

不包含逻辑处理的代码可屏蔽:

  • 实体类(DTO、Entity、VO)。
  • 框架自动生成代码,如 Mapper、实体类等。
  • 枚举类。

用例强制要求#

  • 测试用例必须增加断言逻辑,避免恒真,用例需要保持结果准确性校验,用例执行结果必须能根据代码变化而反映出变化。
  • 单元测试需要尽可能的覆盖函数的所有范围,针对代码执行成功、失败、异常三种情况编写不同的用例。
  • 保证单元测试的独立性。
  • 单元测试是可重复执行的。
  • 测试力度足够小,可以精确定位问题。

用例建议要求#

  • 不建议调用数据库,外部接口,建议使用Mock
  • 不建议启动 Spring 容器。
  • 测试用例均需需为 public void。
  • 单元测试包结构和源码结构尽量保持一致。
  • 单元测试文件名称是由被”测试文件 + Test“ 组成

单元测试用例思路#

以目标类或类中的某一个函数为单元体,通过构造尽可能覆盖所有的单数范围的不同入参对其进行调用,对比返回值是否到达预期,从而验证函数逻辑的正确性。

当然存在被测试类A调用其他类B的函数,为了控制A中的代码逻辑,需要控制B对象函数的返回值,可以根据如下考虑:

B对象来源场景A对象是否需哟啊提前给B赋值赋值方式是否需要Mock函数
静态类静态类初始化或其函数调用过程中,不依赖环境信息(API调用、DB调用、磁盘读取等)不涉及不涉及不需要
静态类静态类初始化或其函数调用过程中,依赖了环境信息(API调用、DB调用、磁盘读取等)不涉及不涉及需要
new 所得静态类初始化或其函数调用过程中,不依赖环境信息(API调用、DB调用、磁盘读取等)不需要不涉及不需要
new 所得静态类初始化或其函数调用过程中,依赖了环境信息(API调用、DB调用、磁盘读取等)需要Mock whenNew需要
A的public方法传入静态类初始化或其函数调用过程中,不依赖环境信息(API调用、DB调用、磁盘读取等)需要public方法不需要
A的public方法传入静态类初始化或其函数调用过程中,依赖了环境信息(API调用、DB调用、磁盘读取等)需要public方法需要
Spring注解静态类初始化或其函数调用过程中,不依赖环境信息(API调用、DB调用、磁盘读取等)需要public 方法需要
Spring注解静态类初始化或其函数调用过程中,依赖了环境信息(API调用、DB调用、磁盘读取等)需要Mock 注解需要

Mock#

Mock 是在测试过程中对于一些不容易构造获取的对象,创建一个Mock对象来模拟对象的行为。基本原理就是先模拟对象,然后声明行为,最后执行验证,通过Mock能力控制代码路径,跳过外部依赖,实现分支覆盖。

Mock 框架目前也有很多版本,有如下几种:

  • Easy Mock:一套通过简单方法对于给定的接口生成 Mock 对象的类库,它提供接口的模拟,能够通过录制、回放、检查三步来完成大体流程,可以令Mock对象返回指定的值或者抛出指定异常。
  • JMock:基于Java开发,大大简化了虚拟对象的使用。
  • Mockito:可读性强,验证语法简单,可以与JUnit无缝结合,是最广泛的Mock框架。
  • PowerMock:Mockito增强版,弥补了对静态方法的不支持。
Easy McokJMockMockitoPowerMock
final 方法不支持不支持支持支持
私有方法不支持不支持不支持不支持
静态方法不支持不支持支持支持
SpringBoot依赖复杂复杂默认依赖基于 Mocktio 拓展
API风格略复杂略复杂简单简单

Mock 单元测试实践#

Mockito#

导入依赖#

Maven

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

Gradle

testImplementation 'org.mockito:mockito-core:<version>'

测试代码#

Mockito 与多种测试框架(如JUnit、TestNG)和依赖注入框架(如Spring)完美集成,所以我们就用最常用的 Spring Boot 代码进行测试。

首先我们先准备好要测试的方法类,内容也是非常的简单,无非是包含了关于用户的 crud 功能。

示例:待测试方法

@Data
@Builder
public class User {
private String id;
private String username;
}
@Repository
public interface IUserMapper {
List<User> getAllUsers();
User getUserById(String id);
void saveUser(User user);
void deleteUser(String id);
}
@Service
public class UserService {
@Autowired
private IUserMapper userMapper;
public List<User> getAllUsers() {
return userMapper.getAllUsers();
}
public User getUserById(String id) {
return userMapper.getUserById(id);
}
public void saveUser(User user) {
userMapper.saveUser(user);
}
public void deleteUser(String id) {
userMapper.deleteUser(id);
}
}

接下来就是写测试类了,我们这里需要依赖 Spring 容器测试,所以我们结合 @SpringBootTest 启动,而具体的测试方法也根据

示例:测试类代码

@SpringBootTest
class UserServiceTest {
@MockBean
private IUserMapper userMapper;
@Autowired
private UserService userService;
@Test
void testGetAllUsers() {
List<User> expectedUsers = Arrays.asList(
UserVo.builder().username("zhangsan").id("1").build(),
UserVo.builder().username("lisi").id("2").build(),
);
Mockito.when(userMapper.getAllUsers()).thenReturn(expectedUsers);
List<User> actualUsers = userService.getAllUsers();
assertEquals(expectedUsers, actualUsers);
Mockito.verify(userMapper).getAllUsers();
}
@Test
void testGetUserById() {
String userId = "1";
User expectedUser = new User(userId, "John");
Mockito.when(userMapper.getUserById(userId)).thenReturn(expectedUser);
User actualUser = userService.getUserById(userId);
assertEquals(expectedUser, actualUser);
Mockito.verify(userMapper).getUserById(userId);
}
@Test
void testSaveUser() {
User user = new User("1", "John");
userService.saveUser(user);
Mockito.verify(userMapper).saveUser(user);
}
@Test
void testDeleteUser() {
String userId = "1";
userService.deleteUser(userId);
Mockito.verify(userMapper).deleteUser(userId);
}
@Test
void testGetUserById_NonExistingUser() {
String userId = "1";
Mockito.when(userMapper.getUserById(userId)).thenReturn(null);
assertThrows(UserNotFoundException.class, () -> {
userService.getUserById(userId);
});
Mockito.verify(userMapper).getUserById(userId);
}
@Test
void testSaveUser_NullUser() {
assertThrows(IllegalArgumentException.class, () -> {
userService.saveUser(null);
});
Mockito.verify(userMapper, Mockito.never()).saveUser(ArgumentMatchers.any());
}
}

常用方法#

mock(Class<T> classToMock)#

创建一个模拟对象,用于代替真实对象的行为。

UserService userServiceMock = Mockito.mock(UserService.class);
when(mock.method()).thenReturn(value)#

定义模拟对象方法的行为,指定当调用方法时应该返回的值。

// 定义当调用 getUserById 方法并传入参数 1 时,返回一个名为 "zhangsan" 的 User 对象
User user = UserVo.builder().username("zhangsan").id("1").build();
Mockito.when(userServiceMock.getUserById(1)).thenReturn(user);
verify(mock).method()#

验证模拟对象的方法是否被调用。

// 验证 getUserById 方法是否被调用
Mockito.verify(userServiceMock).getUserById(1);
verify(mock, times(n)).method()#

验证模拟对象的方法被调用了特定的次数(n)。

// 验证 getUserById 方法被调用了2次
Mockito.verify(userServiceMock, Mockito.times(2)).getUserById(1);
verify(mock, atLeast(n)).method()#

验证模拟对象的方法被调用了至少n次。

// 验证 getUserById 方法被调用了至少3次
Mockito.verify(userServiceMock, Mockito.atLeast(3)).getUserById(1);
verify(mock, never()).method()#

验证模拟对象的方法从未被调用。

// 验证 addUser 方法从未被调用
Mockito.verify(userServiceMock, Mockito.never()).addUser(Mockito.any(User.class));
verifyNoMoreInteractions(mock)#

验证模拟对象上的所有方法已经被验证,并且没有其他未验证的方法调用。

// 验证 userServiceMock 上的所有方法已经被验证,并且没有其他未验证的方法调用
Mockito.verifyNoMoreInteractions(userServiceMock);
doThrow(exception).when(mock).method()#

指定当调用模拟对象的方法时应该抛出的异常。

// 指定当调用 deleteUser 方法并传入任何参数时,抛出一个名为"UserNotFoundException"的异常
Mockito.doThrow(new UserNotFoundException()).when(userServiceMock).deleteUser(Mockito.anyInt());
doAnswer(answer).when(mock).method()#

指定模拟对象方法的调用应该如何进行自定义处理,例如执行回调函数或返回动态计算的结果。

// 定义当调用 updateUser 方法时,执行自定义的逻辑来修改用户对象
Mockito.doAnswer(invocation -> {
User userToUpdate = invocation.getArgument(0);
// 执行自定义逻辑来更新用户对象
userToUpdate.setName("Updated Name");
return userToUpdate;
}).when(userServiceMock).updateUser(Mockito.any(User.class));

PowerMock#

导入依赖#

Maven

<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.9</version>
<scope>test</scope>
</dependency>

Gradle

testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
testImplementation 'org.powermock:powermock-module-junit4:2.0.9'

测试代码#

示例:测试类代码

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* @ClassName TestClass
* @Description 测试 PowerMock 类
*/
public class TestClass {
public String getUUID() {
return UUID.randomUUID().toString();
}
public List<Integer> soutArray() {
return new ArrayList<Integer>() {
{
add(2);
}
};
}
}
package com.sbc.unittest;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* @ClassName TestClassTest
* @Description powermock 测试类
* @Author songbaicheng
* @Date 2023/8/14 12:09
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({TestClass.class, UUID.class})
class TestClassTest {
@Test
public void testGetUUID() throws Exception {
PowerMockito.mockStatic(UUID.class);
PowerMockito.doReturn(new UUID(0L, 0L)).when(UUID.randomUUID());
TestClass testClass = new TestClass();
String uuid = testClass.getUUID();
// PowerMockito.verifyStatic(UUID.class);
assertEquals("00000000-0000-0000-0000-000000000000", uuid);
}
@Test
public void testSoutArray() {
TestClass testClass = PowerMockito.spy(new TestClass());
List mockIntegers = PowerMockito.mock(List.class);
PowerMockito.doReturn(mockIntegers).when(testClass).soutArray();
testClass.soutArray();
}
}

总结#

Mockito 还提供了其他一些高级功能和方法,例如参数匹配、顺序验证、超时验证等,如果想了解更多可以查阅下面的 Mockito的官方文档。

点击跳转官网查看详细内容

测试实践:单元测试规范与质量门禁
https://songbaicheng.cc.cd/posts/unit-test/
作者
宋柏成
发布于
2026-06-05
许可协议
CC BY-NC-SA 4.0