Springboot单元测试编写实践
作者:斜月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单元测试的资料请关注脚本之家其它相关文章!