阿里妹导读:单元测试的好处到底有哪些?每次单测启动应用,太耗时,怎么办?二方三方接口可能存在日常没法用,只能上预发/正式的情况,上预发测低效如何处理?本文分享三个单元测试神器及相关经验总结。
文末福利:《Linux运维学习路线》技术公开课。
软件模块应该只有一个被修改的理由。在大多数情况下,编写Java代码时都会将单一职责原则应用于类。单一职责原则可被视为使封装工作达到最佳状态的良好实践。更改的理由是:需要修改代码。
单一原则,类、方法只干一件事。
模块、类和函数应该对扩展开放,对修改关闭。
通过继承和多态扩展来添加新功能。开闭原则是最重要的设计原则之一,是大多数设计模式的基础。
软件建设一个复杂的结构,当我们完成其中的一部分,就应该不要修改它,而是在其基础上继续建设。
在设计模块和类时,必须确保派生类型从行为的角度来看是可替代的。
使用父类的地方都可以用子类替代。
父类最好为抽象类。
子类可实现父类的非抽象方法,尽量不要覆盖重写已实现的方法。
子类可写自身的方法,有自身的特性,在父类的基础上扩建。
子类覆盖重写父类方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松,后置条件(返回值)要更严格。
减少了代码耦合,使软件更健壮,更易于维护和扩展。
客户端不应该依赖它所不需要的接口。
高级模块不应该依赖低级模块,两者都应该依赖抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
DRY:不要干重复的事儿。
KISS:不要干复杂的事儿,思从深而行从简。
YAGNI:不要干不需要的事儿,尺度把握尤为重要,超越尺度则会有过度设计之嫌。
LOD:最小依赖。
高内聚:相近功能放在同一类中,相近功能往往会被同时修改,放到同一个类中在修改时,代码更易维护(指导类本身的设计)
松耦合:类与类之间的依赖关系简单清晰,一个类的代码改动不会或者很少导致依赖类的代码修改(指导类间依赖关系设计)
单 元测试(unit testing),指由开发人员对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
来源:https://baike.baidu.com/item/单元测试
测试环境没问题,线上怎么就不行。
所有异常捕获,一切尽在掌控(你以为你以为的是你以为的)。
祖传代码,改个小功能(只有上帝知道)。
.....
流程判读符合预期,按照步骤运行,逻辑正确。
执行结果符合预期,代码执行后,结果正确。
异常输出符合预期,执行异常或者错误,超越程序边界,保护自身。
代码质量符合预期,效率,响应时间,资源消耗等。
代码可测性差
方法封装不合理
流程不合理
设计漏洞等
易写单测的方法一定是简单好理解的,可读性是高的,反之难写的单测代码是复杂的,可读性差的。
如设计不合理可微重构,保证代码的可读性以及健壮性。
经过单元测试,能让程序员对自己的代码质量更有信心,对实现方式记忆更深。
不用重复启动Pandora容器,浪费大量时间在容器启动上,方便逻辑验证。
在HSF控制台中只能保存一套参数,而单测可保存多套参数,覆盖各个场景,多条分支,就是一个个测试用例。
CodeReview时作为重点CR的地方
好的单测可作为指导文档,方便使用者使用及阅读
写起来,相信你会发现更多单测带来的价值。
必须要有默认文件。
要编写获取文件的路径的方法。
/*** 向阿里云的OSS存储中存储文件 (改动前)** @param client OSS客户端* @param file 上传文件* @return String 唯一MD5数字签名*/private static void uploadObject2Oss(OSS client, File file, String bucketName, String dirName) throws Exception {InputStream is = new FileInputStream(file);String fileName = file.getName();Long fileSize = file.length();//创建上传Object的MetadataObjectMetadata metadata = new ObjectMetadata();metadata.setContentLength(is.available());metadata.setCacheControl("no-cache");metadata.setHeader("Pragma", "no-cache");metadata.setContentEncoding("utf-8");metadata.setContentType(getContentType(fileName));metadata.setContentDisposition("filename/filesize=" + fileName + "/" + fileSize + "Byte.");//上传文件client.putObject(bucketName, dirName + PublicConstant.DIAGONAL_CHARACTER + fileName, is, metadata);}
/*** 向阿里云的OSS存储中存储文件(改动后)** @param client OSS 上传client* @param bucketName bucketName* @param dirName 目录* @param is 输入流* @param fileName 文件名* @param fileSize 文件大小* @throws Exception*/private void uploadObject2Oss(OSS client, String bucketName, String dirName, InputStream is, String fileName,long fileSize) throws Exception {//创建上传Object的MetadataObjectMetadata metadata = new ObjectMetadata();metadata.setContentLength(is.available());metadata.setCacheControl("no-cache");metadata.setHeader("Pragma", "no-cache");metadata.setContentEncoding("utf-8");metadata.setContentType(getContentType(fileName));metadata.setContentDisposition("filename/filesize=" + fileName + "/" + fileSize + "Byte.");//上传文件client.putObject(bucketName, dirName + PublicConstant.DIAGONAL_CHARACTER + fileName, is, metadata);}
构造sourceInfos列表
构造String数组
构造map对象
构造List
构造User 对象
/*** 按比例混排结果 (改动前)* @param sourceInfos 渠道配比信息* @param resultMap 结果* @param pageSize 总条数* @param aliuid 用户id* @return 结果集*/private List<String> getResultList(List<String[]> sourceInfos, Map<String, List<String>> resultMap, int pageSize, User user) {Map<String, Integer> sourceNumMap = new HashMap<>(sourceInfos.size());sourceInfos.stream().forEach(s -> sourceNumMap.put(s[0], Integer.parseInt(s[1]) * pageSize / 100));List<String> resultList = new ArrayList<>();resultMap.forEach((s, strings) -> resultList.addAll(strings.stream().limit(sourceNumMap.get(s)).collect(Collectors.toList())));// 弥补条数,防止数据量不足if (resultList.size() < pageSize) {compensate(resultList, pageSize, user.getAliuid());}return resultList;}
构造List列表
构造SourceInfo对象
/*** 按比例混排结果* @param sourceInfos 渠道配比信息* @param pageSize 条数* @param aliuid 用户id* @return 结果集*/private List<String> getResultList(List<SourceInfo> sourceInfos, int pageSize, String aliuid) {// 获取结果集List<String> resultList = sourceInfos.stream().flatMap(sourceInfo -> {int needNum = (int)(sourceInfo.getSourceRatio() * pageSize / 100);return listSource(sourceInfo.getSourceChannel(), needNum, aliuid).stream();}).collect(Collectors.toList());// 补偿数据compensate(resultList, pageSize, aliuid());return resultList;}
<dependency><groupId>com.alibaba</groupId><artifactId>fast-tester</artifactId><version>1.3</version><scope>test</scope></dependency>
/*** @author QZJ* @date 2020-08-03*/public class TestApplication {public static void main(String[] args){PandoraBootstrap.run(args);ConfigurableApplicationContext context = SpringApplication.run(TestApplication.class, args);// 将ApplicationContext传给FastTesterFastTester.run(context);}}
/*** tip:添加注解及方法需要重新启动应用** @author QZJ* @date 2020-08-03*/public class BucketServiceTest {BucketService bucketService;public void testSaveBucketInfo() {BucketRequest bucketRequest = new BucketRequest();// 缺少参数bucketRequest.setAccessKeyId("123");bucketRequest.setAccessKeySecret("123");bucketRequest.setBucketDomain("123");bucketRequest.setEndpoint("123");bucketRequest.setRegionId("123");bucketRequest.setRoleArn("123");bucketRequest.setRoleSessionName("123");Result<Long> result = bucketService.saveBucketInfo(bucketRequest);log.info("缺少参数 result :{}", JSON.toJSONString(result));// bucketName 重复bucketRequest.setBucketName("video2sky");result = bucketService.saveBucketInfo(bucketRequest);log.info("bucketName 重复 result :{}", JSON.toJSONString(result));// 正例(执行后,则bucketName已存在,需更换bucketName)bucketRequest.setBucketName("12345");result = bucketService.saveBucketInfo(bucketRequest);log.info("正例 result :{}", JSON.toJSONString(result));}public void testCreateBucketFolder() {BucketFolderRequest bucketFolderRequest = new BucketFolderRequest();bucketFolderRequest.setFolderPath("/test");bucketFolderRequest.setAppName("wudao");bucketFolderRequest.setDescription("data");bucketFolderRequest.setWriteTokenExpireTime(3600L);Result<Long> result = bucketService.createBucketFolder(bucketFolderRequest);log.info("缺少参数 result :{}", JSON.toJSONString(result));// 错误的bucketIdbucketFolderRequest.setBucketId(1L);result = bucketService.createBucketFolder(bucketFolderRequest);log.info("错误的bucketId result :{}", JSON.toJSONString(result));// 异常的读时间,读写时间不得超过2小时bucketFolderRequest.setWriteTokenExpireTime(7300L);result = bucketService.createBucketFolder(bucketFolderRequest);log.info("异常的读时间 result :{}", JSON.toJSONString(result));// 重复的bucketFolderbucketFolderRequest.setBucketId(11L);bucketFolderRequest.setWriteTokenExpireTime(3500L);result = bucketService.createBucketFolder(bucketFolderRequest);log.info("重复的bucketFolder result :{}", JSON.toJSONString(result));// 正例 (本地与服务器默认文件地址不一致,所以本地无法执行成功,除非改地址,或者添加分支代码)bucketFolderRequest.setFolderPath("/test2");result = bucketService.createBucketFolder(bucketFolderRequest);log.info("正例 result :{}", JSON.toJSONString(result));}}
JUnit是一个Java语言的单元测试框架, Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。继承TestCase类,就可以用Junit进行自动测试。
来源:https://baike.baidu.com/item/白盒测试
/*** 普通类测试,无需启动容器** @author QZJ* @date 2020-08-05*/public class OssServiceTest {private OssServiceImpl ossService = new OssServiceImpl();public void testCreateOssFolder() {try {// 私有方法测试:方法一:用反射(推荐);方法二:修改类中方法属性(不推荐)Method method = OssServiceImpl.class.getDeclaredMethod("createOssFolder",new Class[] {OSS.class, String.class, String.class});method.setAccessible(true);OSS client = new OSSClientBuilder().build("oss-cn-beijing.aliyuncs.com", "**","****");Object obj = method.invoke(ossService, new Object[] {client, "***", "wudao/test3"});Assert.assertEquals(true, obj);} catch (Exception e) {Assert.fail("testCreateOssFolder fail");}}}
/*** 普通工具类测试* @author QZJ* @date 2020-08-05*/4jpublic class DateUtilTest {// 忽略该方法执行结果public void testGetCurrentTime(){String dateStr = DateUtil.getCurrentTime("yyyy-MM-dd HH:mm");log.info("date:{}", dateStr);Assert.assertEquals("2020-08-05 17:22", dateStr);}// 方法超时时间设置以及期望执行抛出的异常类型设置(错误的日期格式解析异常)(timeout = 110L, expected = ParseException.class)public void testString2Date() throws ParseException{Date date = DateUtil.string2Date("20202-02 02:02");log.info("date:{}" , date);//Thread.sleep(200L);}public static void beforeClass() {log.info("before class");}public static void afterClass() {log.info("after class");}public void before() {log.info("before");}public void after() {log.info("after");}public static void main(String[] args) {// 不需启动容器的情况下使用,跑类中所有caseResult result = JUnitCore.runClasses(DateUtilTest.class);result.getFailures().stream().forEach(f -> System.out.println(f.toString()));log.info("result:{}", result.wasSuccessful());}}
<dependency><groupId>org.mockito</groupId><artifactId>mockito-all</artifactId><version>1.9.5</version><scope>test</scope></dependency>
@Overridepublic ConsumeCodeResult consumeCode(String code) {// 权益核销if (code.startsWith(BENEFIT_CENTER_CODE_HEADER) && BENEFIT_CENTER_CODE_LENGTH == code.length()) {return consumeCodeFromCodeBenefitCenter(code);}// 码商核销return consumeCodeFromCodeCenter(code);}/*** 从权益中心核销电子凭证** @param code 电子码* @return 核销结果*/private ConsumeCodeResult consumeCodeFromCodeBenefitCenter(String code) {// 参数构造BenefitUseDTO benefitUseDTO = new BenefitUseDTO();benefitUseDTO.setCouponCode(code);benefitUseDTO.getExtendFields().put("configId", benefitId);benefitUseDTO.getExtendFields().put("type", BenefitTypeEnum.CODE_GENERAL.getType().toString());AlispResult alispResult = benefitService.useBenefit(benefitUseDTO);log.info("benefitUseDTO:{}, result:{}", benefitUseDTO, alispResult);if (alispResult.isSuccess()) {BenefitUseResult benefitUseResult = (BenefitUseResult)alispResult.getValue();return new ConsumeCodeResult(benefitUseResult.getOutOrderId(),String.valueOf(benefitUseResult.getConfigId()), benefitUseResult.getUseTime());}// 已使用if (BizErrorCodeEnum.BENEFIT_RECORD_USED.name().equals(alispResult.getErrCodeName())) {throw new BizException(StudentErrorEnum.VERIFICATION_CODE_REPEAT);} else if (BizErrorCodeEnum.BENEFIT_RECORD_NOT_EXIST.name().equals(alispResult.getErrCodeName())|| BizErrorCodeEnum.BENEFIT_RECORD_EXPIRED.name().equals(alispResult.getErrCodeName())) {// 不存在或者过期throw new BizException(StudentErrorEnum.VERIFICATION_CODE_INVALID);} else {// 其他异常throw new BizException(StudentErrorEnum.VERIFICATION_CODE_CONSUME_FAILED);}}
public void mockConsume(){BenefitService benefitService = Mockito.mock(BenefitService.class);// 核销成功链路AlispResult alispResult = new AlispResult(true);BenefitUseResult benefitUseResult = new BenefitUseResult();benefitUseResult.setConfigId(1L);benefitUseResult.setOutOrderId("lalala");benefitUseResult.setUseTime(new Date());alispResult.setValue(benefitUseResult);Mockito.when(benefitService.useBenefit(Mockito.any(BenefitUseDTO.class))).thenReturn(alispResult);ConsumeCodeService consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");ConsumeCodeResult consumeCodeResult = consumeCodeService.consumeCode("082712345678");System.out.println(JSON.toJSONString(consumeCodeResult));alispResult = new AlispResult(false);// 已核销链路alispResult.setErrCodeName("BENEFIT_RECORD_USED");// 已过期链路//alispResult.setErrCodeName("BENEFIT_RECORD_EXPIRED");// 码不存在链路//alispResult.setErrCodeName("BENEFIT_RECORD_NOT_EXIST");// 其他返回错误//alispResult.setErrCodeName("LALALA");Mockito.when(benefitService.useBenefit(Mockito.any(BenefitUseDTO.class))).thenReturn(alispResult);consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");try {consumeCodeService.consumeCode("082712345678");} catch (Exception e) {e.printStackTrace();}// 核销码头有误consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");try {consumeCodeService.consumeCode("081712345678");} catch (Exception e) {e.printStackTrace();}// 核销码长度有误consumeCodeService = new ConsumeCodeServiceImpl(benefitService, "1");try {consumeCodeService.consumeCode("08271234567");} catch (Exception e) {e.printStackTrace();}}
函式覆盖率(Function coverage):有呼叫到程式中的每一个函式(或副程式)吗?
指令覆盖率(Statement coverage):若用控制流图(英语:control flow graph)表示程式,有执行到控制流图中的每一个节点吗?
判断覆盖率(Decision coverage):(和分支覆盖率不同)若用控制流图表示程式,有执行到控制流图中的每一个边吗?例如控制结构中所有IF指令都有执行到逻辑运算式成立及不成立的情形吗?
条件覆盖率(Condition coverage):也称为谓词覆盖(predicate coverage),每一个逻辑运算式中的每一个条件(无法再分解的逻辑运算式)是否都有执行到成立及不成立的情形吗?条件覆盖率成立不表示判断覆盖率一定成立。
条件/判断覆盖率(Condition/decision coverage):需同时满足判断覆盖率和条件覆盖率。
必要的
复杂的
重要的
不写无用的
团队无单测习惯,个人是否follow
业务压力大,觉得写单测耗时
觉得可有可无
单测是一个程序员的自我修养
5 大学习阶段、20 门免费课程、175 课时教学视频、12 套自测考试,带大家掌握使用虚拟机安装Linux,以及Linux常用命令、文件及用户管理、文本处理、Vim工具使用等。
点击“阅读原文”,去学习吧~