一、什么是单元测试
“在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。 在过程化编程中,一个单元就是单个程序、函数、过程等; 对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。 ”
摘录来自 维基百科
单元测试(Unit Testing)顾名思义就是测试一个单元,这里的单元通常指一个函数或类,区别于集成测试中的模块和系统。集成测试的测试过程通常存在跨系统模块的调用,是一种端到端的测试;而单元测试关注对象的颗粒度较小,用来保障一个类或者函数是否按照预期正确的执行。
二、为什么要写单元测试
2.1 减少BUG,释放资源
2.2 为代码重构保驾护航
2.3 既是编写单测也是CodeReview
2.4 便于调试与验证
2.5 驱动设计与重构
三、怎样编写单元测试
3.1 单元测试框架的构建
3.1.1 单元测试框架JUnit
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>
<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 单元测试类的规范
3.2.2.1 测试方法的命名
public void should_returnFalse_when_deleteContent_given_invokeFailed() {...}
public void testDeleteContent() {...}
public void should_returnFalse_when_deleteContent_given_invokeFailed() {// givenResult<Boolean> deleteDocResult = new Result<>();deleteDocResult.setEntity(Boolean.FALSE);when(docManageService.deleteContentDoc(anyLong())).thenReturn(deleteDocResult);when(docManageService.queryContentDoc(anyLong())).thenReturn(new DocEntity());// whenLong contentId = 123L;Boolean result = contentService.deleteContent(contentId);// thenverify(docManageService, times(1)).queryContentDoc(contentId);verify(docManageService, times(1)).deleteContentDoc(contentId);Assert.assertFalse(result);}
3.3 单测方法的示例
public class SnsFeedsShareServiceImpl {private SnsFeedsShareHandler snsFeedsShareHandler;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();}}// 省略代码...}
(MockitoJUnitRunner.class)public class SnsFeedsShareServiceImplTest {SnsFeedsShareHandler snsFeedsShareHandler;SnsFeedsShareServiceImpl snsFeedsShareServiceImpl;public void should_returnServiceError_when_shareFeeds_given_invokeFailed() {// givenResult<Boolean> invokeResult = new Result<>();invokeResult.setSuccess(Boolean.FALSE);invokeResult.setModel(Boolean.FALSE);when(snsFeedsShareHandler.batchShareFeeds(anyLong(), anyString(), anyList())).thenReturn(invokeResult);// whenLong feedsId = 123L;String platform = "TEST_SNS_PLATFORM";List<String> snsAccountList = Collections.singletonList("TEST_SNS_ACCOUNT");Result<List<String>> result = snsFeedsShareServiceImpl.shareFeeds(feedsId, platform, snsAccountList);// thenverify(snsFeedsShareHandler, times(1)).batchShareFeeds(feedsId, platform, snsAccountList);Assert.assertNotNull(result);Assert.assertEquals(result.getResponseCode(), ResponseEnum.SNS_SHARE_SERVICE_ERROR.getResponseCode());}}
3.4 单测的编码技巧
@RunWith(MockitoJUnitRunner.class)public class ContentServiceTest {@MockDocManageService docManageService;@InjectMocksContentService contentService;...}
3.4.2.1 Mock无返回值方法
doNothing().when(contentService.deleteContent(anyLong()));
// givenResult<Boolean> deleteResult = new Result<>(Boolean.FALSE);when(contentService.deleteContent(anyLong())).thenReturn(deleteResult);
when(contentService.deleteContent(anyLong())).thenCallRealMethod();
when(contentService.deleteContent(anyLong())).thenThrow(NullPointerException.class);
3.4.3.1 验证依赖方法的调用
// 验证调用方法的入参,指定为"testTagId"verify(tagOrmService).queryByValue("testTagId");// 验证queryByValue方法被调用了2次verify(tagOrmService, times(2)).queryByValue(anyString());
// thenAssert.assertNotNull(result);Assert.assertEquals(result.getResponseCode(), 200);// 其他常用的断言函数Assert.assertTrue(...);Assert.assertFalse(...);Assert.assertSame(...);Assert.assertEquals(...);Assert.assertArrayEquals(...);
MockedStatic<TagHandler> tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("tag");
(MockitoJUnitRunner.class)public class ContentServiceTest {DocManageService docManageService;ContentService contentService;private static MockedStatic<TagHandler> tagHandlerMockedStatic = null;public static void beforeTest() {tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("testTag");}// 省略测试方法public static void afterTest() {tagHandlerMockedStatic.close();}}
(MockitoJUnitRunner.class)public class ContentServiceTest {DocManageService docManageService;ContentService contentService;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");// 省略单测方法具体实现...}}}
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());}
@Testpublic void should_returnNull_when_select_given_invalidQueryCondition() throws Exception {// whenTableService tableService = mock(TableService.class, RETURNS_DEEP_STUBS);when(tableService.select().from(anyString()).where(any()).limit(anyInt())).thenReturn(null);Object result = lindormClient.select(new QueryCondition());// thenAssert.isNull(result);}
3.5 单测生成插件
四、如何落地单元测试
4.1 清晰单测的价值认知
4.2 将单测纳入流程规范
4.3 单测工作量评估
五、后记
消息服务MNS产品评测征集令发布!
写下你的使用体验,就有机会获得定制冲锋衣、蓝牙音箱、定制鲁班锁、520元云原生通用代金券、消息队列MNS免费使用半年等多重好礼。
点击阅读原文查看详情。