谈谈如何使用好单元测试这把武器
阿里妹导读
本文作者结合我们日常的工作,讨论如何使用好单元测试这把武器。
前言
学习单元测试不应该仅仅停留在技术层面,比如你喜欢的测试框架,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、单测里时间相关的内容
@RunWith(PowerMockRunner.class)@PrepareForTest({Calendar.class, ImpServiceTest.class})public class ImpServiceTest {@InjectMocksprivate ImpService impService = new ImpServiceImpl();@Beforepublic 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 的异常
@RunWith(PowerMockRunner.class)@PrepareForTest({DataEntry.class})public class MockTair {@Mockprivate DataEntry dataEntry;@Beforepublic 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 solutionPowerMockito.whenNew(DataEntry.class).withNoArguments().thenReturn(dataEntry);}@Testpublic 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怎么写单测
@RunWith(PandoraBootRunner.class)@DelegateTo(SpringRunner.class)public class EventProcessorTest {@InjectMocksprivate EventProcessor eventProcessor;@Mockprivate DynamicService dynamicService;@Mockprivate MetaProducer dynamicEventProducer;@Testpublic void dynamicDelayConsumer() throws MQClientException, RemotingException, InterruptedException, MQBrokerException {//获取beanMetaPushConsumer metaPushConsumer = eventProcessor.dynamicEventConsumer();//获取ListenerMessageListenerConcurrently messageListener = (MessageListenerConcurrently)metaPushConsumer.getMessageListener();List<MessageExt> list = new ArrayList<>();//这个需要依赖PandoraBootRunnerMessageExt 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@RunWith(PowerMockRunner.class)// 2.使用PandoraBootRunner, 启动pandora,使用tair,metaq等@RunWith(PandoraBootRunner.class)// 3. springboot启动,加入context上下文,可以直接获取bean@SpringBootTest(classes = {TestApplication.class})
7、尽量使用ioc
@Servicepublic class LoginServiceImpl implements LoginService{public Boolean login(String username, String password,String ip) {// 校验ipif (!IpUtil.verify(ip)) {return false;}/*other func*/return true;}}
通过IpUtil校验登录用户的ip信息,而如果我们这样使用,就需要测试 IpUtil的方法, 违背了隔离性的原则。测试login方法也需要加入更多组测试数据覆盖工具类代码,耦合度太高。
@Servicepublic class LoginServiceImpl implements LoginService{public Boolean login(String username, String password,String ip) {// 校验ipif (!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方法可能会造成抛出异常。
@Test(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
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。