Springboot单元测试编写实践

 更新时间:2023年11月03日 08:26:17   作者:斜月86  
在日常的开发过程中,为了提高代码的可靠性和健壮性,同时也是检测代码的质量,减少测试环节的问题,会对完成的业务功能代码编写单元测试,在本文中,将分享一些单元测试的实践和心得,需要的朋友可以参考下

1 前言

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

2 生成单元测试

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

<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 方法,具体的操作如下所示:

// 第一步需要在测试类上添加类名称
@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 来实现的。

// 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 来返回对应的异常。

// 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 所涉及的方法。

ValueOperations operations = mock(ValueOperations.class);
when(operations.increment(anyString())).thenReturn(230L);
when(redisTemplate.opsForValue()).thenReturn(operations);

3.5 mock 事务 transactionTemplate

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

// 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

// 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) 进行测试,校验其方法调用的次数。

// 断言返回对象不为空
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 异常用例

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

// 单测期望抛出一个异常信息
@Test(expected = RuntimeException.class)
public void testMockTest_TransactionTemplateThrowsTransactionException() throws Exception {
 ...
 // 异常操作      
when(mockTransaction.execute(any(TransactionCallback.class))).thenThrow(RuntimeException.class);
  ....
}

3.9 测试类的 setUp

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

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

4 总结

在本文中,主要介绍了编写单元测试的实践,通过 squaretest 插件可以解决大部分的测试场景,如果有测试覆盖不到的地方,无外乎以上介绍的几种特殊的场景。掌握了以上的方式,可以很轻松的将单测覆盖率提高到一个比较高的水平。

以上就是Springboot单元测试编写实践的详细内容,更多关于Springboot单元测试的资料请关注脚本之家其它相关文章!

相关文章

  • Maven添加Tomcat插件实现热部署代码实例

    Maven添加Tomcat插件实现热部署代码实例

    这篇文章主要介绍了Maven添加Tomcat插件实现热部署代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • protobuf简介及使用流程

    protobuf简介及使用流程

    本文介绍了Protocol Buffers(protobuf)的数据结构序列化和反序列化框架,包括其特点、使用流程和快速上手,通过一个简单的通讯录示例,展示了如何创建.proto文件、添加注释、编写消息定义、编译.proto文件以及进行序列化和反序列化操作,感兴趣的朋友一起看看吧
    2025-02-02
  • SpringBoot如何使用mail实现登录邮箱验证

    SpringBoot如何使用mail实现登录邮箱验证

    在实际的开发当中,不少的场景中需要我们使用更加安全的认证方式,同时也为了防止一些用户恶意注册,我们可能会需要用户使用一些可以证明个人身份的注册方式,如短信验证、邮箱验证等,这篇文章主要介绍了SpringBoot如何使用mail实现登录邮箱验证,需要的朋友可以参考下
    2024-06-06
  • springboot接收json数据时,接收到空值问题

    springboot接收json数据时,接收到空值问题

    这篇文章主要介绍了springboot接收json数据时,接收到空值问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • Java实现简单邮件发送

    Java实现简单邮件发送

    这篇文章主要介绍了Java实现简单邮件发送的相关资料,实例讲解了java邮件发送实现方法,感兴趣的小伙伴们可以参考一下
    2016-02-02
  • idea日志乱码和tomcat日志乱码问题的解决方法

    idea日志乱码和tomcat日志乱码问题的解决方法

    这篇文章主要介绍了idea日志乱码和tomcat日志乱码问题的解决方法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-08-08
  • Java String 对象(你真的了解了吗)

    Java String 对象(你真的了解了吗)

    这篇文章主要介绍了Java String 对象(你真的了解了吗),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-10-10
  • 阿里、华为、腾讯Java技术面试题精选

    阿里、华为、腾讯Java技术面试题精选

    这篇文章主要为大家分享了阿里、华为、腾讯Java技术面试题精选,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-11-11
  • SpringCloud让微服务实现指定程序调用

    SpringCloud让微服务实现指定程序调用

    这篇文章主要介绍了SpringCloud让微服务实现指定程序调用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • springboot项目打docker镜像实例(入门级)

    springboot项目打docker镜像实例(入门级)

    最近做个项目,我们想把自己的程序打包成镜像,并运行在docker容器中,本文主要介绍了springboot项目打docker镜像实例,具有一定的参考价值,感兴趣的可以了解一下
    2024-06-06

最新评论