前言
大家常说:“代码和人,有一个能跑就行”,但这并不意味着我们可以放弃职业道德。与土木工程一样,软件工程也需要一些可度量的指标来衡量产品的交付质量。一个高质量的软件绝对不能只靠测试人员来保证,更不能相信程序员自己立下的“军令状”。
本文将从程序员的角度,讲述开发阶段如何保证软件质量:
- 硬性代码度量指标(静态代码分析,漏洞扫描)
- 自动化测试(Unit Test, Integration Test, Specification Test, E2E Test)
- 交付文档(程序说明文档,SOUP文档,Safety文档)
- 持续集成测试环境
硬性指标
静态代码分析
相信很多同学都被屎山代码折磨过,使用静态代码分析可以将长篇累牍的垃圾代码扼杀在摇篮里。
以Sonarqube为例,它支持几十种编程语言,其中包含数百种C#代码规则,对不满足管理员要求的代码将给出警告,因此经常在Pipeline CI 阶段充当代码质量网关,它可以自动检查pr中的不良代码,添加comments,并生成质量报告。
使用严格的代码审阅工具可以“强迫”开发者写出易于维护的规范代码,例如:
- 要求每个函数不得超过40行,圈复杂度不超过10,结构深度不超过3;
- 代码必须通过单元测试,并且行覆盖率或条件覆盖率必须达到80%;
- 代码与注释的比例不得超过10:1;
- 不能使用魔法数字,比如 if(a == 3), 它会要求你将3定义为一个有意义常量等;
- 警告相同的字符串或者相似的代码片段在程序中出现多次;
-
它还会手把手教你写代码,例如: IDisposable接口的最佳实现。
- Sonarqube | C# static code analysis
漏洞扫描
2021年Java开源日志组件Log4j爆出“核弹级”安全漏洞,该日志组件在全球范围内广泛使用,一时引起轩然大波。
你有没有想过自己项目当中引用了多少种类库呢?这些类库本身又依赖了哪些类库?你如何保证这些类库是安全的?并不是每一个风险漏洞都会上热搜,构建高质量软件必须引入漏洞扫描机制。
同样的,我们需要在Pipeline CI阶段集成漏洞扫描工具,在pr阶段发现程序,程序依赖项,以及依赖项的依赖项,容器镜像等,其中的高/中/低不同安全等级的漏洞,开发者可以依照建议进行修复,软件交付的重要指标就是不存在漏洞库里已知的安全漏洞。
自动化测试
自动化测试同样可以作为Pipeline CI的一部分,无法通过测试验证的代码,不应该允许Merge;无法通过E2E 测试的软件版本,也不应允许发布。
每一个合格的项目都应该具备Unit Test,根据需要添加Integration Test,Specification Test更接近黑盒测试,而E2E Test则是利用页面自动化测试手段,完全模仿用户行为的一种测试方法。
.NET中常用的测试框架有xUnit,MSTest, NSubstitute,Shouldly,Specflow等。.NET 测试框架 xUnit,MSTest, Specflow 使用经验汇总_郭麻花的博客-CSDN博客
Unit Test
单元测试是纯函数测试,它只关心测试模块的输入和输出是否满足要求。单元测试必须保证单一性,纯洁性,不能使用真实的网络请求,不能使用真实的数据库连接,也不可以读取系统当前时间等等。
初识单元测试的同学可能体会不到它的价值,通常会有以下问题:
- 拿着Mock出来的数据库或者网络请求测试有什么意义呢?
- 一个加法函数,输入两个1,结果肯定等于2呀,为什么还要写单元测试?
- 一个几百行的业务函数,调用了N次网络接口,查询了N次数据库,单元测试怎么写?
- 单元测试比代码还多,以后改了需求还要维护单元测试,岂不是要累死?
- ...
这里不多做解释。单元测试是软件质量的基础保证,99%的项目都无法避免非纯函数,这是事实,但这绝不是抵触单元测试的理由。你会发现责任不清晰,逻辑混乱,又臭又长的代码很难进行单元测试,强制要求项目达到80%的单元测试覆盖率可以倒逼开发者重构不良代码。
单元测试可以保证业务逻辑和算法的正确性,还可以提高代码质量,增加日后重构代码时的信心。
Integration Test
开发人员习惯将项目分为不同的模块,这样的代码结构易于管理和维护。Integration Test指的是同一项目中不同模块之间的统一测试。集成测试也应该避免使用真实的数据环境,除非你的项目大部分内容都是与外部集成,并且要保证每次的测试环境都是干净的。
集成测试按照设计方案可以分为全量测试,增量测试,三明治测试。
全量测试指的是将系统作为一个整体进行测试,这种方案需要更全面的测试用例,需要投入更多的人力物力,所以往往在开发阶段后期进行,而且它更关注整体因此不能测试到所有细节。
开发阶段更常用的是增量测试。增量测试一般可分为自顶向下和自底向上两种方式,可以按照系统的开发进度,逐渐集成新的测试模块。或者按照项目的代码逻辑,从最底层或最顶层代码开始测试,然后逐层增加测试模块,最终完成系统整体测试。
分层混合测试又称为三明治测试,三明治测试按照侧重点又可分为高层测试,底层测试和集成测试。高层测试关注用户接口,底层测试接近单元测试关注实现细节,集成测试则是两者结合测试。
Specification Test
Specification Test接近于黑盒测试,一般通过设计具有实际意义的Scenario来进行测试。在Specification Test当中可以使用Mock的外部数据,也可以使用真实的数据库或者其它基础设施。但是一定要控制好测试边界,将关注点放在当前代码项目上,尽量减少对外部系统的依赖,并且最好使用容器化技术,保证每次Specification Test都是在干净的测试环境下进行。
E2E Test
E2E全称为End to End,是软件产品级别的测试。它通过模拟用户的真实操作,确保软件功能符合用户预期。E2E Test需要由测试和产品负责人主导测试用例的设计,由测开或者开发负责实现。
它也适合作为GitOps流程中的smoke test,常用的测试框架有Cypress,WinAppDriver等。
软件文档
软件文档也是持续交付的一部分,对于一些要求严格的项目来说,docs文件夹是必不可少的。
有些甚至会在Pipeline的CI阶段检查pr中是否包含docs的改动,因为每一个pr必须是有目的,必定是带来了变化的,这些变化都必须在文档中体现。
这部分会介绍软件开发阶段应产出哪些文档,还有如何对软件产品进行风险评估等。
程序项目文档
每个项目都包含docs文件夹,例如我们所熟悉的README.md, 完整的代码项目文件应该包含
- 主要功能以及启动运行方式
- 各个模块功能介绍
- API接口定义文档
- 包含的依赖项说明
- 版本变更历史
对于DDD来讲,项目文档还应描述清楚当前domian所处的位置作用,以及定义了哪些领域事件等等。
SOUP分析文档
SOUP全称是Software Of Unknown Provenance,即未知来源软件,这里的未知来源可以指开源产品,也可以指任何第三方软件供应商。SOUP分析就是要对第三方程序进行风险评估。
一般来说,程序中引用的任何第三方程序集都必须通过漏洞扫描,并且要进行SOUP分析。SOUP分析一般从以下几个方面进行:
- 分析程序供应商,程序属于商业产品或是开源项目,项目所在位置,开源许可证类型,主要功能等。
- 使用了程序集的哪些功能,哪些版本,以及程序集的缺陷修复历史等
- 程序集目前仍存在的issue,defect
- 评估程序集的稳定性,安全性,兼容性。
- 评估当前程序集某个功能模块出现故障时,开发或用户能否及时发现,代码中是否有保护措施,会不会对系统安全造成重大影响等。
Safety文档
Safety分析是指对一个系统或产品进行风险评估和安全性分析,它将贯穿整个软件生命周期,确保系统或产品在任何使用场境下都能够保持高水平的安全性,保护用户财产安全,或是人身安全。Safety分析不是一次分析,一份文档能够搞定的,开发需要同其他人员一起对不同软件模块,系统整体逐个进行分析。
Safety分析的方法和技术包括HAZOP、FMEA等。一般来讲,我们需要对系统的每个组成部分、每个变量、每个操作进行审查假设,包括代码bug,网络故障,硬件故障,用户误操作,数据输入或处理不当,外部接口模块故障等各种各样异常类型。
列举出可能的故障之后,需要评估故障发生概率,故障发生后的严重程度,故障是否有保护措施及时终止,故障能否被用户及时发现等等。
通常会用风险评估表以供相关人员参考,根据以上分析内容得出一个风险等级表,并且撰写具体的分析报告。
持续集成测试环境
最后一个部分是必须有一套完备的集成测试方案,这需要来自Devops团队的支持。
很多内容需要在CI阶段完成,例如: 集成代码扫描,自动化测试等。
另外对于Specification Test和E2E Test,二者对测试环境要求比较高。为了保证纯净的测试环境,最好有一套针对软件产品的自动化集群部署方案。保证每次测试环境都是完备纯净的,并且要能够能够自动化开展测试,定时测试,自动上传并可视化测试结果等。
稳定的集成测试环境是保证软件工程质量最艰难,最重要的一个环节,因此很多公司都会投入精力做好这个环节。