谈谈如何使用好单元测试这把武器
阿里妹导读
本文作者结合我们日常的工作,讨论如何使用好单元测试这把武器。
前言
学习单元测试不应该仅仅停留在技术层面,比如你喜欢的测试框架,mocking 库等等,单元测试远远不止「写测试」这件事,你需要一直努力在单元测试中投入的时间回报最大化,尽量减少你在测试中投入的精力,并最大化测试提供的好处,实现这两点并不容易。
单元测试的定义
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。至于【单元】的含义,一般来说,要根据实际情况判定具体含义,如Java里单元指一个类等。
85%的缺陷都在代码设计阶段产生;
发现bug的阶段越靠后,耗费成本就越高,呈指数级别的增长。
常见的误区
如何写出好的单测
语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%。 --- 《阿里巴巴Java开发手册》 单测覆盖度分级参考 Level1:正常流程可用,即一个函数在输入正确的参数时,会有正确的输出 Level2:异常流程可抛出逻辑异常,即输入参数有误时,不能抛出系统异常,而是用自己定义的逻辑异常通知上层调用代码其错误之处 Level3:极端情况和边界数据可用,对输入参数的边界情况也要单独测试,确保输出是正确有效的 Level4:所有分支、循环的逻辑走通,不能有任何流程是测试不到的 Level5:输出数据的所有字段验证,对有复杂数据结构的输出,确保每个字段都是正确的
最佳实践
1、隐藏的测试边界值
public ApiResponse<List<Long>> getInitingSolution() {
List<Long> solutionIdList = new ArrayList<>();
SolutionListParam solutionListParam = new SolutionListParam();
solutionListParam.setSolutionType(SolutionType.GRAPH);
solutionListParam.setStatus(SolutionStatus.INIT_PENDING);
solutionListParam.setStartId(0L);
solutionListParam.setPageSize(100);
List<OperatingPlan> operatingPlanList = operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
for(; !CollectionUtils.isEmpty(operatingPlanList);){
/*
do something
*/
solutionListParam.setStartId(operatingPlanList.get(operatingPlanList.size() - 1).getId());
operatingPlanList = operatingPlanMapper.queryOperatingPlanListByType(solutionListParam);
}
return ResponsePackUtils.packSuccessResult(solutionIdList);
}
上面这段代码,如何写单元测试?
2、不要在springboot测试中使用@Transactional以及操作真实数据库
3、单测里时间相关的内容
public class ImpServiceTest {
private ImpService impService = new ImpServiceImpl();
public void setup(){
MockitoAnnotations.initMocks(this);
Calendar now = Calendar.getInstance();
now.set(2022, Calendar.JULY, 2 ,0,0,0);
PowerMockito.mockStatic(Calendar.class);
PowerMockito.when(Calendar.getInstance()).thenReturn(now);
}
}
4、final类,static类等的单元测试
5、应用启动报 Can not load this fake sdk class 的异常
(PowerMockRunner.class)
({DataEntry.class})
public class MockTair {
private DataEntry dataEntry;
public void hack() throws Exception {
//solve it should be loaded by Pandora Container. Can not load this fake sdk class. please refer to http://gitlab.alibaba-inc.com/middleware-container/pandora-boot/wikis/faq for the solution
PowerMockito.whenNew(DataEntry.class).withNoArguments().thenReturn(dataEntry);
}
public void mock() throws Exception {
String value = "value";
PowerMockito.when(dataEntry.getValue()).thenReturn(value);
DataEntry tairEntry = new DataEntry();
//值相等
Assert.assertEquals(value.equals(tairEntry.getValue()));
}
}
6、metaq怎么写单测
(PandoraBootRunner.class)
(SpringRunner.class)
public class EventProcessorTest {
private EventProcessor eventProcessor;
private DynamicService dynamicService;
private MetaProducer dynamicEventProducer;
public void dynamicDelayConsumer() throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//获取bean
MetaPushConsumer metaPushConsumer = eventProcessor.dynamicEventConsumer();
//获取Listener
MessageListenerConcurrently messageListener = (MessageListenerConcurrently)metaPushConsumer.getMessageListener();
List<MessageExt> list = new ArrayList<>();
//这个需要依赖PandoraBootRunner
MessageExt messageExt = new MessageExt();
list.add(messageExt);
Event event = new Event();
event.setUserType(3);
String text = JSON.toJSONString(event);
messageExt.setBody(text.getBytes());
messageExt.setMsgId(""+System.currentTimeMillis());
//测试consumeMessage方法
messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
doThrow(new RuntimeException()).when(dynamicService).triggerEventV2(any());
messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
messageExt.setBody(null);
messageListener.consumeMessage(list, new ConsumeConcurrentlyContext(new MessageQueue()));
}
}
总结一下什么时候使用容器:
// 1. 使用PowerMockRunner
// 2.使用PandoraBootRunner, 启动pandora,使用tair,metaq等
// 3. springboot启动,加入context上下文,可以直接获取bean
7、尽量使用ioc
public class LoginServiceImpl implements LoginService{
public Boolean login(String username, String password,String ip) {
// 校验ip
if (!IpUtil.verify(ip)) {
return false;
}
/*
other func
*/
return true;
}
}
通过IpUtil校验登录用户的ip信息,而如果我们这样使用,就需要测试 IpUtil的方法, 违背了隔离性的原则。测试login方法也需要加入更多组测试数据覆盖工具类代码,耦合度太高。
public class LoginServiceImpl implements LoginService{
public Boolean login(String username, String password,String ip) {
// 校验ip
if (!IpUtil.verify(ip)) {
return false;
}
/*
other func
*/
return true;
}
}
这样我们只需要单独测试IpUtil类和LoginServiceImpl类就行了。测试LoginServiceImpl的时候mock掉IpUtil就可以了,这样就隔离了IpUtil的实现。
8、不要为了覆盖率测没意义的代码
9、如何测试void方法
如果void方法内部造成了数据库的变更,比如insertPlan(Plan plan),并通过H2操作过数据库,那么可以验证数据库的条数变化等,校验void方法的正确性。
如果void方法调用了函数,可以通过verify验证方法得到调用次数:
userService.updateName(1L,"qiushuo");
verify(mockedUserRepository, times(1)).updateName(1L,"qiushuo");
如果void方法可能会造成抛出异常。
(expected = InvalidParamException.class)
public void testUpdateNameThrowExceptionWhenIdNull() {
doThrow(new InvalidParamException())
.when(mockedUserRepository).updateName(null,anyString();
userService.updateName(null,"qiushuo");
}
参考资料
1、https://scottming.github.io/2021/04/07/unit-testing/
2、https://docs.spring.io/spring-framework/docs/current/reference/html/testing.html#integration-testing
3、https://yuque.antfin-inc.com/fangqintao.fqt/pu2ycr/eabim6
4、https://yuque.antfin-inc.com/aone613114/en7p02/pdtwmb
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
微信扫码关注该文公众号作者