Springboot 单元测试编写实践

江南有丹橘,经冬犹绿林。

1 前言

在日常的开发过程中,为了提高代码的可靠性和健壮性,同时也是检测代码的质量,减少测试环节的问题,会对完成的业务功能代码编写单元测试。有时间单元测试的覆盖率也是工作的一部分。作者最近被安排了一项艰巨的单测任务,在本文中,将分享一些单元测试的实践和心得。

2 生成单元测试

通常情况下,单元测试都是使用 junit 编写的,但是这种方式会真实的调用数据,如何优雅的实现单元测试是一个问题。这里使用的是 powermock 来实现测试用例的编写。引入 powermock 依赖如下所示:

xml 复制代码
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

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

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

在编写单元测试之前,需要在 idea 中安装一个 squaretest 插件,在需要编写单元测试的类中,通过右键 generate -> generate Test 可以打开一个面板,可以选择一个模板,就可以在test中生成具体的单元测试类。

可以这样说,有了这个便捷的插件助力,对于简单的场景,就可以完全覆盖其业务功能。

3 单测重点

虽然说通过 squaretest 可以实现大部分代码的编写,但是有一些场景还不能那么智能的实现,需要编写代码实现功能。如下图所示,除了第三个单测点外,其它的都是不常见的类型。

3.1 mock static 方法

static 方法的 mock 需要使用到 mockStatic 方法,具体的操作如下所示:

scss 复制代码
// 第一步需要在测试类上添加类名称
@PrepareForTest(value = {类名.class})
// 第二步需要使用 mockStatic 声明,然后进行mock 操作
mockStatic(类名.class)
when(类名.static方法()).thenReturn(mock结果);

static 方法的 mock, 适用于需要加载系统配置或者初始化文件的场景,通过 mock 类的 static 方法,即可获取相应的返回值,避免类的初始化导致单元测试报错。

3.2 mock 分布式锁

分布式锁的 mock, 需要考虑的方面比较多,首先是根据 redisclient 获取分布式锁,然后调用 tryLock 并等待加锁的返回信息,这里其实是需要两个 mock, 但是 getLock 返回的 RLock 是一个接口,不能使用创建新类的方式来实现,这里就需要使用 mock() 来创建一个 RLock, 然后在其基础上进行 mock , 由此可以实现两层的 mock。这里需要说明的是,分布式锁使用的 Redisson 来实现的。

scss 复制代码
// mock RLock
RLock mock = mock(RLock.class);
// mock tryLock 和 getLock 两个方法
when(mock.tryLock(anyLong(), eq(TimeUnit.MINUTES))).thenReturn(Boolean.TRUE);
when(mockRedisUtils.getLock(anyString())).thenReturn(mock);
3.3 mock spring 中的 bean

在单元测试中常用的 mock 即测试类中注入的 service、business、mapper 等内容,通过 mock 所涉及的方法,以期得到对应的返回值继续业务流程的继续。 when 和 thenReturn 需要组合使用,根据传入的方法参数返回预期值。方法的入参可以是 any(), any(类.class), anyString(), anyInt(), anyLong() 等,但需要注意的是传入的参数不能为 null,如果需要精确匹配,则需要使用 eq()。此外, thenReturn 的返回值可以有多个,支持链式调用,如果返回值有多个,则表示第一次调用方法返回第一个值,第二次调用方法返回第二个值,以此类推。另外,还有模拟方法调用发送异常的场景,则使用 doThrow 来返回对应的异常。

less 复制代码
// mock 业务查询和操作
when(mockMapper.selectByUserId(eq("123"))).thenReturn(user);
when(mockMapper.selectByUserId(anyString())).thenReturn(user, user, user);
when(mockMapper.selectByUserId(anyString())).thenReturn(user).thenReturn(user).thenReturn(user);
when(mockMapper.updateById(any(User.class))).thenReturn(1);
// 模拟调用抛出异常
doThrow(RuntimeException.class).when(mockUserMapper).selectByUserId(anyString());
3.4 mock redis template 操作

对于 redis 的操作,其实和分布式锁的操作类似,以操作字符串类型为例,需要先获取一个 opsForValue 而后来进行操作 redisTemplate.opsForValue().方法,由于 ValueOperations 也是一个接口,所以需要使用 mock 来获取一个操作对象,基于此在进行 mock 所涉及的方法。

scss 复制代码
ValueOperations operations = mock(ValueOperations.class);
when(operations.increment(anyString())).thenReturn(230L);
when(redisTemplate.opsForValue()).thenReturn(operations);
3.5 mock 事务 transactionTemplate

事务的操作是在特殊业务场景下才会用到,这里的只是借此场景来说明如何对匿名类中的方法进行单测。默认情况下生成的测试代码是不具备这种能力的,需要使用 thenAnswer 来处理。以下列举了两种方式,一种是匿名类的方式,一种是 lambda 表达式的方法。根据以下方式操作,功能内部类中代码可以实现覆盖。如果项目中使用了线程池,也同样可以依据此方法处理。

scss 复制代码
// mock transaction
when(mockTransaction.execute(any(TransactionCallback.class))).thenReturn(true);
// 匿名内部类
when(mockTransaction.execute(Mockito.<TransactionCallback>any())).thenAnswer(new Answer<Object>() {
    public Object answer(InvocationOnMock invocation) {
        Object[] args = invocation.getArguments();
        TransactionCallback arg = (TransactionCallback) args[0];
        return arg.doInTransaction(new SimpleTransactionStatus());
    }
});
// lambda 方式
Answer<Object> answer = invocation -> {
    Object[] args = invocation.getArguments();
    TransactionCallback arg = (TransactionCallback) args[0];
    return arg.doInTransaction(new SimpleTransactionStatus());
};
when(mockTransaction.execute(any())).thenAnswer(answer);
3.6 mock http ResetTemplate

通常情况下 http 的调用是使用 httpUtils 工具类, 但是特殊的情况下使用 restTemplate 进行调用,如下所示,可以实现对 http 调用的 mock,这里只是一种 post 的调用方式,如果有其他的类型调用可以参考编写 mock

less 复制代码
// mock http reset http
JSONObject body = new JSONObject();
body.put("code", "0000");
when(restTemplate.postForObject(anyString(), any(HttpEntity.class), eq(JSONObject.class))).thenReturn(body);
3.7 断言

在编写完成单元测试后,需要对结果进行断言,通常情况下每个方法都需要有一个断言,针对 void 方法,可以使用 verify 来进行处理,校验方法中的某一个环节是否被处理过。现在项目的集成与发版都实现了自动化,没有断言或者 verify 的方法会扫描出存在漏洞。断言可以分为返回对象不为空或者返回值和预期值相同与否。verify 可以添加 times(1) 进行测试,校验其方法调用的次数。

scss 复制代码
// 断言返回对象不为空
Assert.assertNotNull(result);
// 断言结果的期望值和结果值相同
Assert.assertEquals(result, expectedResult);
// 断言方法中的某个环节被执行过 调用一次
// times(n) 调用 n 次
// never() 没有调用,相当于 调用 0 次 times(0)
// atMostOnce() 最多调用一次
// atLeastOnce() 最少调用一次
// atLeast() 最少一次
// atMost() 最多一次
verify(mockUserMapper, times(2)).selectByUserId(anyString());
verify(mockUserMapper, atLeast(1)).selectByUserId(anyString());
3.8 异常用例

以上讲述的都是正常的单测,在实际的业务中还要模拟一些异常的场景,所以需要异常用例的编写也是需要的,这样进入到异常场景也可以提高单测的覆盖率。

csharp 复制代码
// 单测期望抛出一个异常信息
@Test(expected = RuntimeException.class)
public void testMockTest_TransactionTemplateThrowsTransactionException() throws Exception {
 ...
 // 异常操作      
when(mockTransaction.execute(any(TransactionCallback.class))).thenThrow(RuntimeException.class);
  ....
}
3.9 测试类的 setUp

通常情况下,在复杂的业务场景,需要对测试类设置属性值,一般情况下属性值都是从配置文件读取,那怎么对其设置属性值呢?这里用到了反射的知识,通过 hutool 工具类,可以对类的某个属性赋值。同时也可以在这里做一些初始化的操作或者测试类单测前的准备工作。

typescript 复制代码
@Before
public void setUp() {
    initMocks(this);
    // 使用反射的方式设置对象属性的值
    ReflectionTestUtils.setField(mockBusinessUnderTest, "name", "test");
}

4 总结

在本文中,主要介绍了编写单元测试的实践,通过 squaretest 插件可以解决大部分的测试场景,如果有测试覆盖不到的地方,无外乎以上介绍的几种特殊的场景。掌握了以上的方式,可以很轻松的将单测覆盖率提高到一个比较高的水平。本文中所涉及的代码已经上传至 github, 欢迎交流学习。项目地址 springboot-auth

相关推荐
loveLifeLoveCoding几秒前
Java List sort() 排序
java·开发语言
草履虫·7 分钟前
【Java集合】LinkedList
java
AngeliaXue9 分钟前
Java集合(List篇)
java·开发语言·list·集合
世俗ˊ10 分钟前
Java中ArrayList和LinkedList的比较
java·开发语言
zhouyiddd15 分钟前
Maven Helper 插件
java·maven·intellij idea
攸攸太上23 分钟前
Docker学习
java·网络·学习·docker·容器
Milo_K31 分钟前
项目文件配置
java·开发语言
程序员大金34 分钟前
基于SpringBoot+Vue+MySQL的养老院管理系统
java·vue.js·spring boot·vscode·后端·mysql·vim
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS网上购物商城(JAVA毕业设计)
java·vue.js·spring boot·后端·开源
nsa652231 小时前
Knife4j 一款基于Swagger的开源文档管理工具
java