跳转到正文
宋柏成
返回

单元测试

单元测试

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

引言

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

质量门襟要求

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

规范的单元测试

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

屏蔽内容限制

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

用例强制要求

用例建议要求

单元测试用例思路

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

当然存在被测试类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 McokJMockMockitoPowerMock
final 方法不支持不支持支持支持
私有方法不支持不支持不支持不支持
静态方法不支持不支持支持支持
SpringBoot依赖复杂复杂默认依赖基于 Mocktio 拓展
API风格略复杂略复杂简单简单

Mock 单元测试实践

Mockito

导入依赖

code-tabs @tab Maven#Maven
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <scope>test</scope>
</dependency>

@tab Gradle#Gradle

testImplementation 'org.mockito:mockito-core:<version>'
#### 测试代码 Mockito 与多种测试框架(如JUnit、TestNG)和依赖注入框架(如Spring)完美集成,所以我们就用最常用的 Spring Boot 代码进行测试。

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

待测试方法 ```java @Data @Builder public class User { private String id; private String username; } ``` ```java @Repository public interface IUserMapper { List getAllUsers();
User getUserById(String id);

void saveUser(User user);

void deleteUser(String id);

}

```java
@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 启动,而具体的测试方法也根据
测试类代码 ```java @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());
}

}


<details>
<summary>#### 常用方法</summary>
##### mock(Class<*T*> classToMock) 
创建一个模拟对象,用于代替真实对象的行为。

```java
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

导入依赖

code-tabs @tab Maven#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>

@tab Gradle#Gradle

    testImplementation 'org.powermock:powermock-api-mockito2:2.0.9'
    testImplementation 'org.powermock:powermock-module-junit4:2.0.9'
#### 测试代码
测试类代码 ```java 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 soutArray() {

     return new ArrayList<Integer>() {
         {
             add(2);
         }
     };

    } }

```java
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的官方文档。

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


分享这篇文章:

上一篇
个人图床
下一篇
Vue3