“在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。” 摘录来自维基百科
单元测试(Unit Testing)顾名思义就是测试一个单元,这里的单元通常指一个函数或类,区别于集成测试中的模块和系统。集成测试的测试过程通常存在跨系统模块的调用,是一种端到端的测试;而单元测试关注对象的颗粒度较小,用来保障一个类或者函数是否按照预期正确的执行。
作为保障代码质量的有效手段之一,公司也在积极的推进单元测试。结合单测的实践,总结了以下几点单元测试的好处,认真实践过的同学,应该会有共鸣。
2.1 减少BUG,释放资源
上面这张图,旨在说明两个问题:
单元测试是所有测试环节中最底层的一类测试,是第一个环节,也是最重要的一个环节。大多数缺陷是Coding阶段引入,修复的成本随着软件生命周期进展不断上升。日常研发中,在交付测试前我们对功能单元进行主流程、各种边界及异常单元测试的编写,能有效帮助我们发现代码中的缺陷。相对于后期来自测试同学或者线上异常反馈,再来进行排查定位、修复发布的成本来说,单元测试的性价比是极高的。单元测试可以有效地保障代码质量,给我们带来质量口碑的同时,也为他人和自己减少因修复低级BUG而投入的时间,能够将精力分配到其他更有意义的事情上。
2.2 为代码重构保驾护航
面对项目中历史遗留的腐化代码,我们都有推倒重来的冲动,但它毕竟经过了长时间的稳定性考验,我们又担心重构之后出现问题。这是我们经常会遇到的境况,当要重构不是非常熟悉的祖传代码,又没有充足的测试资源保障的时候,重构引入缺陷的风险还是很大的。
那如何保证重构不出错呢?Martin Fowler在《重构:改善既有代码的设计》提到:
重构是很有价值的工具,但只有重构还不行。要正确地进行重构,前提是得有一套稳固的测试集合,以帮我发现难以避免的疏漏。即便有工具可以帮我自动完成一些重构,很多重构手法依然需要通过测试集合来保障。
除了需要对业务流程有足够的了解并且熟练掌握各种设计思想、模式之外,单元测试是保证重构不出错的有效手段。当重构完成之后,如果新的代码仍然能通过单元测试,那就说明代码原有正确的逻辑未被破坏,原有的外部可见行为没有发生改变。单元测试给了我们重构的信心与底气。
2.3 既是编写单测也是CodeReview
单元测试和CR是保障代码质量行之有效的两个手段。在研发交付过程中,通常我们提交CR的时机较为滞后,评审同学指出待优化或修复的时间点也较晚,修复的风险和成本上都有所增加。
我们编写编码单元测试过程,其实也是自我CodeReview的过程。在这个过程中,我们对功能单元主流程、边界及异常进行测试,也在自我审视代码的规范、逻辑及设计。既提高了后续提交CR的质量与评审效率,也将问题提前暴露。
2.4 便于调试与验证
当项目存在多个协同方时,我们只需按照约定mock出依赖项的数据,无需等所有依赖的应用接口开发部署完成后再进行调试,提高了我们协同的效率与质量。我们将功能需求进行拆解,在开发完每一个小功能点时,即可进行单元测试的编写与验证,这种习惯能让我们对编码得到快速的验证反馈;同时,在开发完整个功能时,我们需要跑一遍项目所有的单测用例,可以清晰的感知,本次整个功能需求的改动是否对已有业务case造成影响。
如果我们能够保障每个类、函数都能通过单元测试按照预期业务逻辑执行,那整合后的功能模块或系统,出问题的概率都能大大降低。从这个意义上讲,单元测试也对集成测试、系统测试做了有力的支撑。
2.5 驱动设计与重构
设计和编码的时候,我们很难将所有的问题都想清楚。那我们知道,评判代码质量重要的的标准之一就是代码的可测性。如果对一段代码进行单测,发现难于编写,需要编写的case非常多,或者当前的测试框架无法mock依赖对象,需要依赖其他具备高级特性的测试框架时,我们需要回过头来审视代码,是否编码设计得不合理,导致代码的可测性不高。这是个正反馈的过程,让我们有针对性的进行重新设计与重构。
3.1 单元测试框架的构建
3.1.1 单元测试框架JUnit
JUnit是目前Java语言应用最为广泛的单元测试框架,用于编写和运行可重复的自动化测试,它包含以下特性:
多数Java的开发环境都已经集成了JUnit作为单元测试的工具,开源框架对JUnit 都有相应的支持
3.1.2 单元测试Mock框架
项目中依赖关系往往往非常复杂,单元测试Mock框架做的事就是模拟被测试类的依赖项,提供预期的行为和状态,使得我们的单测可以聚焦在被测试类本身,而不必受到依赖项的复杂度的影响。
这里我们讨论常用的Mockito与PowerMock,两者都是作为单元测试模拟框架,模拟应用中复杂的依赖对象。Mockito基于动态代理的方式实现,PowerMock在Mockito基础上增加了类加载器以及字节码篡改技术,使其可以实现完成对private/static/final方法的Mock。
公司使用JaCoCo来做单元覆盖率的检测,当我们使用支持字节码篡改的mock工具的时候,可能会造成:
所以我们推荐使用Mockito来作为我们的单元测试Mock框架,原因有二: 1、在版本3.4.0以后,Mockito支持静态方法的mock。并且作为SpringBootTest默认集成的Mock工具,所以建议大家使用高版本的Mockito,并通过它来完成静态方法的Mock。 2、不提倡使用PowerMock,并不是一味追求单测覆盖率,而是当我们需要使用到具备高级特性mock工具时,我们需要审视代码的合理性,并尝试进行优化重构,使其具备较好的可测性。
3.1.3 依赖引入
3.1.3.1 添加JUnit的maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
3.1.3.2 单测Mock框架的引入
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.7.0</version>
<scope>test</scope>
</dependency>
3.2 单测方法的命名
3.2.1 单元测试类的规范
ContentService -> ContentServiceTest
3.2.2 单元测试方法规范
3.2.2.1 测试方法的命名
好的单元测试方法名,能让我们快速知道测试的场景、意图及验证的预期。 建议采用should_{预期结果}_when_{被测方法}_given_{给定场景} 举个例子:
@Test
public void should_returnFalse_when_deleteContent_given_invokeFailed() {
...
}
反例:
@Test
public void testDeleteContent() {
...
}
3.2.2.2 单测方法实现分层 单测方法的实现如果分层清晰,能让代码便于理解,一目了然,同时也能提高后续的CR的效率。 这里我们建议采用given-when-then的三段落结构。 举个例子:
@Test
public void should_returnFalse_when_deleteContent_given_invokeFailed() {
// given
Result<Boolean> deleteDocResult = new Result<>();
deleteDocResult.setEntity(Boolean.FALSE);
when(docManageService.deleteContentDoc(anyLong())).thenReturn(deleteDocResult);
when(docManageService.queryContentDoc(anyLong())).thenReturn(new DocEntity());
// when
Long contentId = 123L;
Boolean result = contentService.deleteContent(contentId);
// then
verify(docManageService, times(1)).queryContentDoc(contentId);
verify(docManageService, times(1)).deleteContentDoc(contentId);
Assert.assertFalse(result);
}
3.3 单测方法的示例
3.3.1 代码案例
public class SnsFeedsShareServiceImpl {
private SnsFeedsShareHandler snsFeedsShareHandler;
@Autowired
public void setSnsFeedsShareHandler(SnsFeedsShareHandler snsFeedsShareHandler) {
this.snsFeedsShareHandler = snsFeedsShareHandler;
}
public Result<Boolean> shareFeeds(Long feedsId, String platform, List<String> snsAccountList) {
if (!validateParams(feedsId, platform, snsAccountList)) {
return ResponseBuilder.paramError();
}
try {
Result<Boolean> snsResult = snsFeedsShareHandler.batchShareFeeds(feedsId, platform, snsAccountList);
if (Objects.isNull(snsResult) || !snsResult.isSuccess() || Objects.isNull(snsResult.getModel())) {
return ResponseBuilder.buildError(ResponseEnum.SNS_SHARE_SERVICE_ERROR);
}
return ResponseBuilder.successResult(snsResult.getModel());
} catch (Exception e) {
LOGGER.error("shareFeeds error, feedsId:{}, platform:{}, snsAccountList:{}",
feedsId, platform, JSON.toJSONString(snsAccountList), e);
return ResponseBuilder.systemError();
}
}
// 省略代码...
}
3.3.2 单元测试代码案例
@RunWith(MockitoJUnitRunner.class)
public class SnsFeedsShareServiceImplTest {
@Mock
SnsFeedsShareHandler snsFeedsShareHandler;
@InjectMocks
SnsFeedsShareServiceImpl snsFeedsShareServiceImpl;
@Test
public void should_returnServiceError_when_shareFeeds_given_invokeFailed() {
// given
Result<Boolean> invokeResult = new Result<>();
invokeResult.setSuccess(Boolean.FALSE);
invokeResult.setModel(Boolean.FALSE);
when(snsFeedsShareHandler.batchShareFeeds(anyLong(), anyString(), anyList())).thenReturn(invokeResult);
// when
Long feedsId = 123L;
String platform = "TEST_SNS_PLATFORM";
List<String> snsAccountList = Collections.singletonList("TEST_SNS_ACCOUNT");
Result<List<String>> result = snsFeedsShareServiceImpl.shareFeeds(feedsId, platform, snsAccountList);
// then
verify(snsFeedsShareHandler, times(1)).batchShareFeeds(feedsId, platform, snsAccountList);
Assert.assertNotNull(result);
Assert.assertEquals(result.getResponseCode(), ResponseEnum.SNS_SHARE_SERVICE_ERROR.getResponseCode());
}
}
3.4 单测的编码技巧
3.4.1 Mock依赖对象
@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {
@Mock
DocManageService docManageService;
@InjectMocks
ContentService contentService;
...
}
当然我们也可以使用直接初始化或者@Spy的方式来模拟对象,然后使用Setter方法来进行模拟对象的注入,这里介绍了较为简便的方式。
3.4.2 Mock返回值
3.4.2.1 Mock无返回值方法
doNothing().when(contentService.deleteContent(anyLong()));
3.4.2.2 Mock方法返回值
// given
Result<Boolean> deleteResult = new Result<>(Boolean.FALSE);
when(contentService.deleteContent(anyLong())).thenReturn(deleteResult);
3.4.2.3 执行方法的真实调用
when(contentService.deleteContent(anyLong())).thenCallRealMethod();
3.4.2.4 Mock方法调用异常
when(contentService.deleteContent(anyLong())).thenThrow(NullPointerException.class);
3.4.3 自动化验证
3.4.3.1 验证依赖方法的调用
// 验证调用方法的入参,指定为"testTagId"
verify(tagOrmService).queryByValue("testTagId");
// 验证queryByValue方法被调用了2次
verify(tagOrmService, times(2)).queryByValue(anyString());
3.4.3.2 验证返回值
对验证方法的返回值或异常进行验证
// then
Assert.assertNotNull(result);
Assert.assertEquals(result.getResponseCode(), 200);
// 其他常用的断言函数
Assert.assertTrue(...);
Assert.assertFalse(...);
Assert.assertSame(...);
Assert.assertEquals(...);
Assert.assertArrayEquals(...);
3.4.4 其他单测技巧处理3.4.4.1 使用Mockito模拟静态方法
MockedStatic<TagHandler> tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);
tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("tag");
3.4.4.2 处理Mockito注册静态方法范围
在执行mvn test时,如果有多个测试方法mock了Mockito.mockStatic(TagHandler.class),会报错,因为静态方法是类级别的,会出现注册多次的情况。可以参考下面两种解法:
1、**使用@BeforeClass与**@AfterClass
@BeforeClass注解方法:只被执行一次;运行junit测试类时第一个被执行的方法 @AfterClass注解方法:只被执行一次;运行junit测试类时最后一个被执行的方法 示例:
@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {
@Mock
DocManageService docManageService;
@InjectMocks
ContentService contentService;
private static MockedStatic<TagHandler> tagHandlerMockedStatic = null;
@BeforeClass
public static void beforeTest() {
tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);
tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("testTag");
}
// 省略测试方法
@AfterClass
public static void afterTest() {
tagHandlerMockedStatic.close();
}
}
2、 在 try-with-resources 构造中定义模拟
@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {
@Mock
DocManageService docManageService;
@InjectMocks
ContentService contentService;
@Test
public void should_returnEmptyList_when_queryContentTags_given_invokeParams() throws Exception {
try (MockedStatic<TagHandler> tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class)) {
tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("testTag");
// 省略单测方法具体实现
...
}
}
}
3.4.4.3 如何mock一条链式调用
public T select(QueryCondition queryCondition) throws Exception {
LindormQueryParam params = queryCondition.generateQueryParams();
if (Objects.isNull(params)) {
LOGGER.error("Invalid query condition:{}", queryCondition.toString());
return null;
}
Select select = tableService.select()
.from(params.getTableName())
.where(params.getCondition())
.limit(1);
QueryResults results = select.execute();
return convert(results.next());
}
Mockito提供了形如
tableService.select().from(params.getTableName()).where(params.getCondition()).limit(1)
链式调用解决办法,mock对象的时候增加参数RETURNS_DEEP_STUBS
@Test
public void should_returnNull_when_select_given_invalidQueryCondition() throws Exception {
// when
TableService tableService = mock(TableService.class, RETURNS_DEEP_STUBS);
when(tableService.select().from(anyString()).where(any()).limit(anyInt())).thenReturn(null);
Object result = lindormClient.select(new QueryCondition());
// then
Assert.isNull(result);
}
3.5 单测生成插件
IDEA有两款比较好用的单测自动生成插件TestMe[1]与Diffblue[2],这里主要介绍TestMe,如果大家有比较好的插件也可以推荐。
1、安装:在IDEA设置中的Plguins插件里搜索TestMe,下载安装即可。 2、使用:在code按钮找到入口,或者直接使用快捷键option+shift+Q
3、生成的代码如下
自动生成插件方便初始化部分代码,可以提升单测编写的效率,但是也存在局限性:单测名称规范、具体实现等还是需要我们完善、补充后才能正常使用。
4.1 清晰单测的价值认知
不难发现,公司内的项目还是外网开源项目,少有工程具备完善、高质量的单元测试。上文讲了为什么要写单测,这里就不再赘述了。短期来看,单测无疑会带来开发工作量和开发时长的增加,但是我们要从整个迭代周期来看单测的优势。从最终的效果来看,坚持单元测试会有效的减少迭代中的缺陷数以及缩短需求的交付周期。
4.2 将单测纳入流程规范
4.2.1 将单元测试纳入CR标准
以往我们CR只关注核心的业务代码,大多数情况下,我们在评审中可以指出代码较为明显的缺陷或者不合理的设计,但是各种条件case、边界及异常情况很难通过肉眼review出来。如果提交的CR中包含完善、高质量的单元测试,提交、评审双方的的信心都会增强。
4.2.2 发布管控
当我们提交代码后,CI可以设置运行该分支的单元测试。在发布流程中,添加单测相关的管控,比如单元测试通过率以及单元测试增量覆盖率等
4.3 单测工作量评估
对于单元测试工作量的评估,没有一个固定的标准,主要视业务逻辑复杂度而定。一般来说,如果之前没有编写过单元测试,在熟悉阶段可以根据需求的工作量对应增加20%~30%;后期熟练掌握后,增加需求工作量的10%就足够了。当业务需求涉及的case较多,单测需要覆盖这些必要流程时,我们评估工作量时,可以给自己加些时间来保障高质量的单测。
单元测试是一件知易行难的事情,公司也在积极宣导和建设单测文化。工作方式的改变其实难度并不大,难的是能够建立一致的共识,并从心底认可单元测试的价值,只有这样才能有效落地。
参考链接:
[1]https://plugins.jetbrains.com/plugin/9471-testme
[2]https://plugins.jetbrains.com/plugin/14946-diffblue-cover--create-complete-junit-tests-with-ai
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/wQjFlXbK3MqKTUX2TfRR0g
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。