首页 SpringBoot 中的单元测试
文章
取消

SpringBoot 中的单元测试

还是那个军工项目啊,因为对接了第三方平台的接口,但是那边还有没有开发完成,目前只给了接口文档,那咋联调呢?那就基于 mockito 整个单元测试吧,以前写过忘记了,又是各种查资料,现在重新整理出来。

概念

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。至于“单元”的大小或范围,并没有一个明确的标准,“单元”可以是一个函数、方法、类、功能模块或者子系统。单元测试通常和白盒测试联系到一起,如果单从概念上来讲两者是有区别的,不过我们通常所说的“单元测试”和“白盒测试”都认为是和代码有关系的,所以在某些语境下也通常认为这两者是同一个东西。还有一种理解方式,单元测试和白盒测试就是对开发人员所编写的代码进行测试。

这是百度百科来的,通俗的讲,就是测试一个方法逻辑有没有漏洞,执行结果是否符合预期。

代码实操,快速开始

本案例为 SpringBoot 2.7.6 版本,基于 Junit5。Junit5 与 Junit4 注解上略有不同。

  1. 完成正常业务逻辑开发!demo 如下,简单的登录逻辑

    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
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
     @Data
     @Accessors(chain = true)
     public class User {
    
         private Integer id;
         private String name;
         private String password;
    
     }
    
     @Repository
     public class UserDAO {
    
         private List<User> db = Lists.newArrayList(new User().setId(1).setName("用户1").setPassword("pwd1"),
             new User().setId(2).setName("用户2").setPassword("pwd2"));
    
         public User selectById(int id) {
             return db.stream().filter(u -> u.getId().equals(id)).findAny().orElse(null);
         }
    
         public boolean save(User user) {
             return this.db.add(user);
         }
    
     }
    
     @Service
     public class UserService {
    
         @Autowired
         private UserDAO userDAO;
    
         public boolean login(User user) {
             User userInDB = userDAO.selectById(user.getId());
    
             if (user.getPassword().equals(userInDB.getPassword())) {
                 return true;
             }
    
             return false;
         }
    
     }
    
  2. 引入 SpringBoot 提供的单元测试 starter,通过 Spring Initializr 创建的项目一般都会自动引入,无需手动引入。

    1
    2
    3
    4
    5
    
     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
     </dependency>
    
  3. 单元测试代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
     @ExtendWith(SpringExtension.class)
     @SpringBootTest
     class UserServiceTest {
    
         @Autowired
         private UserService userService;
    
         @Test
         void login() {
             User param = new User().setId(1).setPassword("pwd");
             boolean login = userService.login(param);
             assert !login;
         }
    
     }
    

最佳实践,进阶使用

Mockito

mock 普通方法

如此,一个简单的单元测试就完成了,但是前提是在所有代码全部完工的情况下,如果 UserDAO.selectById 方法是调用第三方服务的,并且此时如果第三方服务的接口还没有开发完成?那怎么测试?我们可以通过基于 Mockito 的方式进行,以 UserDAO.selectById 为例,使用 Mockito 创建一个 UserDAO 类型的对应,然后声明 selectById 传入何种参数时的返回结果,然后进行调用,模拟UserDAO 的正常运行,来进行代码逻辑的测试,实例如下。

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
@ExtendWith(SpringExtension.class)
@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    /**
    * 使用 MockBean 创建 UserDAO 对象
    */
    @MockBean
    private UserDAO userDAO;

    @Test
    void login() {
        // 定义当调用 userDAO.selectById 的入参为 1 时,
        // 返回 一个 User id 为 1 密码为 pwd 的 User 对象,
        // 与上文数据库中的id 为 1、密码为 pwd1 的对象做区分。
        Mockito.when(userDAO.selectById(1))
            .thenReturn(new User().setId(1).setName("用户1").setPassword("pwd"));

        User param = new User().setId(1).setPassword("pwd");
        // 此时 login 方法中调用 userDAO.selectById(1) 返回的用户对象密码为 pwd
        // 因此返回结果为 true
        boolean login = userService.login(param);
        assert login;
    }

}

mock 静态方法

有时候,我们要 mock 的方法可能是一个静态方法,那怎么做的?首先需要修改 pom 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <!-- 排除 mockito-core 依赖 -->
<!--     <exclusions>-->
<!--         <exclusion>-->
<!--             <groupId>org.mockito</groupId>-->
<!--             <artifactId>mockito-core</artifactId>-->
<!--         </exclusion>-->
<!--     </exclusions>-->
</dependency>

<!-- 引入 mockito-inline,mockito-inline 依赖了 mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>4.5.1</version>
</dependency>

测试代码如下

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Service
public class UserService {

    @Autowired
    private UserDAO userDAO;

    public boolean loginV2(User user) {
        User userInDB = userDAO.selectById(user.getId());

        /**
        * 使用静态方法进行密码比对
        */
        return PwdChecker.check(user.getPassword(), userInDB.getPassword());
    }

}

public class PwdChecker {

    public static boolean check(String srcPwd, String distPwd) {
        return Objects.equals(srcPwd, distPwd);
    }

}

@ExtendWith(SpringExtension.class)
@SpringBootTest
class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserDAO userDAO;

    @Test
    void login() {
        // 必须在 try-with-resources 语句的作用域中使用,否则 mock 不生效。
        try (MockedStatic<PwdChecker> mockedStatic = Mockito.mockStatic(PwdChecker.class)) {
            // mock 设置调用 PwdChecker.check("pwd", "pwd1") 返回 true
            mockedStatic.when(() -> PwdChecker.check("pwd", "pwd1")).thenReturn(true);
            Mockito.when(userDAO.selectById(1))
                .thenReturn(new User().setId(1).setName("用户1").setPassword("pwd1"));
            User param = new User().setId(1).setPassword("pwd");

            // PwdChecker.check("pwd", "pw1d") 结果 true,所以 login 值为 true
            boolean login = userService.loginV2(param);
            assert login;
        }
    }

}

匹配任意参数

通过 org.mockito.ArgumentMatchersanyXx() 方法,xx 表示类型,如下是匹配任意 int

1
2
3
// 匹配任意 int 值都返回 new User().setId(1).setName("用户1").setPassword("pwd1") 这个对象
Mockito.when(userDAO.selectById(org.mockito.ArgumentMatchers.anyInt()))
                .thenReturn(new User().setId(1).setName("用户1").setPassword("pwd1"));

当某个方法参数是对象时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
public class UserService {

    @Autowired
    private UserDAO userDAO;

    public boolean register(User user) {
        return userDAO.save(user);
    }

}

@Test
void register() {

    Mockito.when(userDAO.save(ArgumentMatchers.any())).thenReturn(false);

    boolean register = userService.register(null);

    assert !register;

}

源码地址:https://github.com/zhangzangqian13/spring-boot-unit-test-demo1/tree/main

本文由作者按照 CC BY 4.0 进行授权

2022-12-14 内炼总结

2022-12-15 内炼总结