一、什么是单元测试
“在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。 程序单元是应用的最小可测试部件。 在过程化编程中,一个单元就是单个程序、函数、过程等; 对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。 ”
摘录来自 维基百科
单元测试(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() {
// 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 单测方法的示例
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() {
// 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 单测的编码技巧
@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {
@Mock
DocManageService docManageService;
@InjectMocks
ContentService contentService;
...
}
3.4.2.1 Mock无返回值方法
doNothing().when(contentService.deleteContent(anyLong()));
// given
Result<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());
// then
Assert.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());
}
@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 单测生成插件
四、如何落地单元测试
4.1 清晰单测的价值认知
4.2 将单测纳入流程规范
4.3 单测工作量评估
五、后记
消息服务MNS产品评测征集令发布!
写下你的使用体验,就有机会获得定制冲锋衣、蓝牙音箱、定制鲁班锁、520元云原生通用代金券、消息队列MNS免费使用半年等多重好礼。
点击阅读原文查看详情。