关于后端:5个编写技巧有效提高单元测试实践

2次阅读

共计 10577 个字符,预计需要花费 27 分钟才能阅读完成。

简介:联合单测的实际,本文总结了几点单元测试的益处与编写技巧,心愿分享给大家。1. 什么是单元测试“在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性测验的测试工作。程序单元是利用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是办法,包含基类、抽象类、或者派生类中的办法。”摘录来自维基百科单元测试(Unit Testing)顾名思义就是测试一个单元,这里的单元通常指一个函数或类,区别于集成测试中的模块和零碎。集成测试的测试过程通常存在跨零碎模块的调用,是一种端到端的测试;而单元测试关注对象的颗粒度较小,用来保障一个类或者函数是否依照预期正确的执行。2. 为什么要写单元测试作为保障代码品质的无效伎俩之一,公司也在踊跃的推动单元测试。联合单测的实际,总结了以下几点单元测试的益处,认真实际过的同学,应该会有共鸣。2.1 缩小 BUG,开释资源

下面这张图,旨在阐明两个问题:85% 的缺点都在代码设计阶段产生;发现 bug 的阶段越靠后,消耗老本就越高,呈指数级别的增长。单元测试是所有测试环节中最底层的一类测试,是第一个环节,也是最重要的一个环节。大多数缺点是 Coding 阶段引入,修复的老本随着软件生命周期停顿一直回升。日常研发中,在交付测试前咱们对性能单元进行主流程、各种边界及异样单元测试的编写,能无效帮忙咱们发现代码中的缺点。绝对于前期来自测试同学或者线上异样反馈,再来进行排查定位、修复公布的老本来说,单元测试的性价比是极高的。单元测试能够无效地保障代码品质,给咱们带来品质口碑的同时,也为别人和本人缩小因修复低级 BUG 而投入的工夫,可能将精力调配到其余更有意义的事件上。2.2 为代码重构保驾护航面对我的项目中历史遗留的腐化代码,咱们都有推倒重来的激动,但它毕竟通过了长时间的稳定性考验,咱们又放心重构之后呈现问题。这是咱们常常会遇到的境况,当要重构不是十分相熟的祖传代码,又没有短缺的测试资源保障的时候,重构引入缺点的危险还是很大的。那如何保障重构不出错呢?Martin Fowler 在《重构:改善既有代码的设计》提到:重构是很有价值的工具,但只有重构还不行。要正确地进行重构,前提是得有一套巩固的测试汇合,以帮我发现难以避免的疏漏。即使有工具能够帮我主动实现一些重构,很多重构手法仍然须要通过测试汇合来保障。除了须要对业务流程有足够的理解并且熟练掌握各种设计思维、模式之外,单元测试是保障重构不出错的无效伎俩。当重构实现之后,如果新的代码依然能通过单元测试,那就阐明代码原有正确的逻辑未被毁坏,原有的内部可见行为没有产生扭转。单元测试给了咱们重构的信念与底气。2.3 既是编写单测也是 CodeReview 单元测试和 CR 是保障代码品质卓有成效的两个伎俩。在研发交付过程中,通常咱们提交 CR 的机会较为滞后,评审同学指出待优化或修复的工夫点也较晚,修复的危险和老本上都有所增加。咱们编写编码单元测试过程,其实也是自我 CodeReview 的过程。在这个过程中,咱们对性能单元主流程、边界及异样进行测试,也在自我扫视代码的标准、逻辑及设计。既进步了后续提交 CR 的品质与评审效率,也将问题提前裸露。2.4 便于调试与验证当我的项目存在多个协同方时,咱们只需依照约定 mock 出依赖项的数据,无需等所有依赖的利用接口开发部署实现后再进行调试,进步了咱们协同的效率与品质。咱们将性能需要进行拆解,在开发完每一个小性能点时,即可进行单元测试的编写与验证,这种习惯能让咱们对编码失去疾速的验证反馈;同时,在开发残缺个性能时,咱们须要跑一遍我的项目所有的单测用例,能够清晰的感知,本次整个性能需要的改变是否对已有业务 case 造成影响。如果咱们可能保障每个类、函数都能通过单元测试依照预期业务逻辑执行,那整合后的功能模块或零碎,出问题的概率都能大大降低。从这个意义上讲,单元测试也对集成测试、零碎测试做了无力的撑持。2.5 驱动设计与重构设计和编码的时候,咱们很难将所有的问题都想分明。那咱们晓得,评判代码品质重要的的规范之一就是代码的可测性。如果对一段代码进行单测,发现难于编写,须要编写的 case 十分多,或者以后的测试框架无奈 mock 依赖对象,须要依赖其余具备高级个性的测试框架时,咱们须要回过头来扫视代码,是否编码设计得不合理,导致代码的可测性不高。这是个正反馈的过程,让咱们有针对性的进行从新设计与重构。3. 怎么编写单元测试 3.1 单元测试框架的构建 3.1.1 单元测试框架 JUnitJUnit 是目前 Java 语言利用最为宽泛的单元测试框架,用于编写和运行可反复的自动化测试,它蕴含以下个性:用于测试冀望后果的断言(Assertion)用于共享独特测试数据的测试工具用于不便的组织和运行测试的测试套件图形和文本的测试运行器少数 Java 的开发环境都曾经集成了 JUnit 作为单元测试的工具,开源框架对 JUnit 都有相应的反对 3.1.2 单元测试 Mock 框架我的项目中依赖关系往往往非常复杂,单元测试 Mock 框架做的事就是模仿被测试类的依赖项,提供预期的行为和状态,使得咱们的单测能够聚焦在被测试类自身,而不用受到依赖项的复杂度的影响。这里咱们探讨罕用的 Mockito 与 PowerMock,两者都是作为单元测试模仿框架,模仿利用中简单的依赖对象。Mockito 基于动静代理的形式实现,PowerMock 在 Mockito 根底上减少了类加载器以及字节码篡改技术,使其能够实现实现对 private/static/final 办法的 Mock。公司应用 JaCoCo 来做单元覆盖率的检测,当咱们应用反对字节码篡改的 mock 工具的时候,可能会造成:测试失败,mock 工具与 jacoco 同时批改字节码时引入的抵触某些类的覆盖率为 0 所以咱们举荐应用 Mockito 来作为咱们的单元测试 Mock 框架,起因有二:在版本 3.4.0 当前,Mockito 反对静态方法的 mock。并且作为 SpringBootTest 默认集成的 Mock 工具,所以倡议大家应用高版本的 Mockito,并通过它来实现静态方法的 Mock 不提倡应用 PowerMock,并不是一味谋求单测覆盖率,而是当咱们须要应用到具备高级个性 mock 工具时,咱们须要扫视代码的合理性,并尝试进行优化重构,使其具备较好的可测性 3.1.3 依赖引入 3.1.3.1 增加 JUnit 的 maven 依赖 Springboot 我的项目 <dependency>

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>

</dependency>SpringMVC 我的项目 <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 单元测试类的标准单元测试类须要放在工程的 test 目录下,比方 xxx/src/test/java 单测类的命名依照标准,应以被测类名结尾,并追加 Test 作为结尾,比方 ContentService  ->  ContentServiceTest3.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;

...

}MockitoJUnitRunner 使 Mockito 的注解失效或者应用初始化办法 MockitoAnnotations.initMocks(this) 利用 @Mock 模仿各种依赖对象应用 @InjectMocks 将 mock 出的依赖对象注入到指标测试对象中。以上述代码为例,单测中将 docManageService 注入到 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),会报错,因为静态方法是类级别的,会呈现注册屡次的状况。能够参考上面两种解法:应用 @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();
}

} 在 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 与 Diffblue,这里次要介绍 TestMe,如果大家有比拟好的插件也能够举荐。装置:在 IDEA 设置中的 Plguins 插件里搜寻 TestMe,下载安装即可。应用:在 code 按钮找到入口,或者间接应用快捷键 option+shift+Q

生成的代码如下

主动生成插件不便初始化局部代码,能够晋升单测编写的效率,然而也存在局限性:单测名称标准、具体实现等还是须要咱们欠缺、补充后能力失常应用 4. 如何落地单元测试 4.1 清晰单测的价值认知不难发现,公司内的我的项目还是外网开源我的项目,少有工程具备欠缺、高质量的单元测试。上文讲了为什么要写单测,这里就不再赘述了。短期来看,单测无疑会带来开发工作量和开发时长的减少,然而咱们要从整个迭代周期来看单测的劣势。从最终的成果来看,保持单元测试会无效的缩小迭代中的缺点数以及缩短需要的交付周期。4.2 将单测纳入流程标准 4.2.1 将单元测试纳入 CR 规范以往咱们 CR 只关注外围的业务代码,大多数状况下,咱们在评审中能够指出代码较为显著的缺点或者不合理的设计,然而各种条件 case、边界及异常情况很难通过肉眼 review 进去。如果提交的 CR 中蕴含欠缺、高质量的单元测试,提交、评审单方的的信念都会加强。4.2.2 公布管控当咱们提交代码后,CI 能够设置运行该分支的单元测试。在公布流程中,增加单测相干的管控,比方单元测试通过率以及单元测试增量覆盖率等

4.3 单测工作量评估对于单元测试工作量的评估,没有一个固定的规范,次要视业务逻辑复杂度而定。一般来说,如果之前没有编写过单元测试,在相熟阶段能够依据需要的工作量对应减少 20%~30%;前期熟练掌握后,减少需要工作量的 10% 就足够了。当业务需要波及的 case 较多,单测须要笼罩这些必要流程时,咱们评估工作量时,能够给本人加些工夫来保障高质量的单测。5. 后记单元测试是一件知易行难的事件,公司也在踊跃宣导和建设单测文化。工作形式的扭转其实难度并不大,难的是可能建设统一的共识,并从心底认可单元测试的价值,只有这样能力无效落地。原文链接:https://click.aliyun.com/m/10… 本文为阿里云原创内容,未经容许不得转载。

正文完
 0