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单元测试的资料请关注脚本之家其它相关文章!

相关文章

  • Java final关键字用法、场景、面试题全解析

    Java final关键字用法、场景、面试题全解析

    本文全程围绕final的核心用法展开,从“修饰变量、修饰方法、修饰类”三个核心场景,搭配实战代码、避坑细节,再补充高频面试题,帮你彻底吃透final,既适合新手入门,也适合巩固基础、备战面试,感兴趣的朋友一起看看吧
    2026-04-04
  • JAVA生成xml文件简单示例

    JAVA生成xml文件简单示例

    这篇文章主要介绍了JAVA生成xml文件的相关资料,在Java中可以使用DOM或者JDOM来生成XML文件,文中通过示例代码介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • Spring Boot 事务详解(事务传播行为、事务属性)

    Spring Boot 事务详解(事务传播行为、事务属性)

    Spring Boot 提供了强大的事务管理功能,通过 @Transactional 注解可以方便地配置事务的传播行为和属性,本文将详细介绍 Spring Boot 中的事务管理,包括事务传播行为、事务属性以及声明式和编程式事务管理,感兴趣的朋友一起看看吧
    2025-05-05
  • mybatis-plus中配置日志信息方式

    mybatis-plus中配置日志信息方式

    这篇文章主要介绍了mybatis-plus中配置日志信息方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03
  • SpringBoot解析JSON报错400 Bad Request的解决方案

    SpringBoot解析JSON报错400 Bad Request的解决方案

    在Spring Boot开发中,400 Bad Request错误是常见的客户端错误之一,本文将从问题分析,排查步骤到解决方案进行系统性讲解,帮助开发者高效定位并修复问题
    2025-07-07
  • Java中启动线程start和run的两种方法

    Java中启动线程start和run的两种方法

    start()方法它的作用是启动一个新线程,run()就和普通的成员方法一样,可以被重复调用。接下来通过本文给大家分享Java中启动线程start和run的两种方法,需要的朋友参考下吧
    2017-11-11
  • java实现创建缩略图、伸缩图片比例生成的方法

    java实现创建缩略图、伸缩图片比例生成的方法

    这篇文章主要介绍了java实现创建缩略图、伸缩图片比例生成的方法,可实现针对图片大小的缩放功能,是Java针对图片操作的典型应用,需要的朋友可以参考下
    2014-11-11
  • maven 中relativePath标签的作用

    maven 中relativePath标签的作用

    元素在 Maven 的 pom.xml 文件中用于指定父项目的相对路径,这篇文章主要介绍了maven 中relativePath标签的作用,需要的朋友可以参考下
    2024-11-11
  • Java实现读写文件功能的代码分享

    Java实现读写文件功能的代码分享

    这篇文章主要为大家详细介绍了如何利用Java语言实现读写文件功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2022-08-08
  • SpringBoot实现短信发送及手机验证码登录

    SpringBoot实现短信发送及手机验证码登录

    本文主要介绍了SpringBoot实现短信发送及手机验证码登录,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07

最新评论