阿里妹导读
作者一年前围绕设计模式与代码重构写了一篇《代码整洁之道 -- 告别码农,做一个有思想的程序员!》的文章。本文作为续篇,从测试角度谈程序员对软件质量的追求。
从一个案例聊聊为什么要做好测试?
北京时间 2024年7月19日中午开始,全球多地用户在推特、脸书、微博等社交平台反映使用微软系统的电脑出现蓝屏现象,至少 20 多个国家的交通、金融、医疗、零售等行业或公共服务的业务系统受到影响,打工人戏称“感谢微软,喜提下班”。微软初步估计此次故障影响了全球近 850 万台设备,全球经济损失总额可能达到 150 亿美元左右。因此,此次故障被称为“历史上最严重的IT故障事件”。此次故障原因是 CrowdStrike 终端安全软件 Falcon 推送的错误的配置引发了内存读取越界,进而导致 Windows 操作系统崩溃。
从 CrowdStrike 复盘报告可以看出测试环境的疏漏是造成此次故障的关键原因。其过度依赖过去的成功,将未充分验证的配置文件进行了大范围推送,改进措施也重点强调了将会优化测试流程提升软件质量。
通过这个例子,可以看出软件质量是企业竞争力的核心要素,决定了用户体验和市场口碑,也是企业立足市场的生命线。而软件测试是保障软件质量的关键环节。通过系统性的方法,软件测试能够及时发现和纠正代码中的缺陷与不足,确保软件在发布前达到预定的质量标准。
正确认识测试
测试角色的迁移:全生命周期覆盖
传统软件开发,行业普遍采用“瀑布模型”,整个软件开发周期严格遵守需求、设计、开发、测试、部署几个阶段。整个流程中,需要上一个阶段工作完成后,才能进入下一阶段。开发、测试、运维有明确的责任边界,每个阶段都有严格的质量和成本把控。但这种模式也存在一些缺陷,产品迭代往往按月进行,导致无法应对快速变化的需求,无法适应互联网行业的发展需求。
在整个流程中测试处于一个承上启下的位置,而严格的边界划分却造成了开发、测试、运维之间的隔阂。在这个背景下,测试左移到开发侧、测试右移到运维侧有效地拉通了整个软件开发环节,极大程度上提升了软件研发效率。
测试左移测试左移(Shift-Left Testing)是一种将软件测试活动尽可能提前到开发生命周期中的做法。这个概念基于一个直观的理念:越早发现和修复缺陷,涉及的成本就越低,并且对最终产品的质量影响越小。采用左移测试的策略,通常包括实施单元测试、集成测试、代码静态分析以及代码审查等措施。它强调预防优于治疗,倡导在代码编写阶段就积极发现潜在的问题。测试右移测试右移(Shift-Right Testing)是一种将软件测试向后延伸至产品发布后的实践,强调在真实的运行环境中对软件进行评估,以便捕获并解决那些在早期测试阶段可能未被发现的问题。通过测试右移,开发团队可以利用监控、日志分析和用户反馈等手段,确保软件在生产环境中的稳定性和可靠性。这种策略有助于捕捉到那些在标准测试环境中不易发现的缺陷,包括与特定硬件配置、网络条件、并发用户交互等相关的问题。以测试左移为例:《代码大全》从软件工程实践视角论证了Bug产生的不同阶段,修复Bug的成本从需求、设计、测试、上线成本存在指数级上升趋势。大部分 Bug 是在开发阶段引入的,因此将测试提前到开发阶段,尽早发现、预防问题;而测试越是到后期,随着产品复杂度的增加,Bug 定位解决的时间成本也就会越来越不可控。
因此,可以明确的是开发(Development)、测试(Testing)与运维(Operations)的融合趋势——即DevOps的实践,正成为行业不可逆转的潮流。在此背景下,测试活动的范畴不断拓展,全面渗透至软件开发生命周期的每一个环节,无所不在地担当起软件质量保障守护者的重任。
穷尽测试是不可能的,那如何破局?
质量(测试的全面性和结果准确性)、效率(测试用例执行效率)、成本(完成测试所需的资源,包括人力与机器成本)是评判测试好坏的关键衡量标准,但是三者之间又构成了一个复杂且微妙的平衡,常被喻为“软件测试不可能三角”,强调了它们之间相互制约、难以同时达到最优状态的关系。
不可能三角的相互影响:三角的任一角正向提升时,都对另外一角或两角产生了负向影响。
提升质量=>提高测试覆盖率=>增加测试点,其他条件相同情况下,会导致效率降低,增加成本。提升效率=>提升执行效率=>提升自动化程度,其他条件相同情况下,会导致项目外的自动化建设成本增加。降低成本=>降低人成本或降低机器成本,两种方式通常都会对质量产生负向影响,效率有可能提升也可能下降。基于此理论,可以看到为了实现软件质量的最大化,而采用穷尽测试是不可能的,因为这样会造成指数级的成本上升。
不可能三角的理论基础指出质量、效率、成本间存在着某种必然的牵制,但是实际测试工作难道真的不能做到三者共同提升吗?其实不尽然,而技术手段就是破局的关键。
上图反映了研发生命周期过程中,成本与收益的变化趋势。新技术在初期付出研发成本,在后期应用时收回收益,质量领域的技术不论是质量收益(不可测变可测),还是效率收益(同等交付下效率更快),都遵循上述投入、回本、持续收益三大阶段的规律。测试成本相比质量效率,价值需要更长的周期,达到成本收益临界点才能全面体现。为了实现尽早的测试成本与收益的平衡,我们可以通过合理的测试方法论的指导以及测试技术手段的运用实现破局:1)测试设计合理性、研发效率提升,减少前期研发投入时间;2)测试工程能力建设(技术可复制性),延长测试收益时间。
如何做好测试
上文介绍了软件测试的理论思想,接下来我们将结合具体的实践,展开介绍相关的测试技术。
测试设计
我们知道穷尽测试是无法做到,但并不是说软件测试就毫无章法可言。相反,通过一些系统性的设计方法,我们可以尽可能地去寻找一些测试数据与测试场景覆盖尽量多的关键测试点,在成本可控的前提下,实现测试质量跟效率的最大化。
常见测试方法有:
测试用例的要素:
编号(必选)标题(必选)优先级(可选)预置条件(可选)操作动作(必选)预期结果(必选)后置动作(可选)等价类划分/边界值分析等价类划分1. 定义:顾名思义,等价类划分,就是将测试的范围划分成几个互不相交的子集,他们的并集是全集,从每个子集选出若干个有代表性的值作为测试用例。
2. 划分:分为有效等价类(合理的、有意义的、系统接受的输入)与无效等价类(不合理的、无意义的、系统不能接受的输入)。
边界值分析定义:大量的错误是发生在输入或输出范围的边界上,而不是发生在输入输出范围的内部。因此针对各种边界情况设计测试用例,可以查出更多的错误;应用场景:如规定了取值范围或规定了取值个数时,或者程序使用了一个内部数据结构,可利用从范围或集合里的边界点进行用例设计考虑;边界值的三点:上点:边界上的点离点:离上点最近的点内点:边界有效范围内的任一点以测试登录框功能正确性为例,这个设计步骤如下:
影响因子提取,以输入参数作为正交因子。等价类划分,有效等价类(合理的、有意义的、系统接受的输入)和无效等价类(不合理的、无意义的、系统不能接受的输入)。等价类取值,转换成可测试的数据。基于影响因子进行用例组合,其中有效等价类取值尽量正交,无效等价类取值命中即可。用例优先级设定,去除优先级较低的用例。场景流程图现代软件很多时候都是用事件触发来控制流程的,事件触发时的情景便形成了场景,而同一事件不同的触发顺序和处理结果就形成事件流。这种在软件设计的思想也可以引入到软件测试中,可以比较生动地描绘出事件触发时的情景,有利于测试设计者设计测试用例,同时使测试用例更容易理解和执行。
定义:根据场景来设计测试用例的方法我们称之为场景法,也称为流程分析法。场景设计三个流程:基本流:通过业务流程输入都为正确的,能够最后达到目标的流程。备选流:通过实现业务流程时,因错误操作或异常输入,导致流程存在反复,但最终能够完成期望业务的流程。异常流:通过实现业务流程时,在错误操作或异常输入,导致业务没有正确完成。3. 使用方法:理解需求,确定业务流程;绘制流程图,明确流程路径。
以 ATM 取款机为例。
根据流程图生成场景完善用例设计错误推断定义:在测试程序时,人们可以根据经验或直觉推测程序中可能存在的各种错误,从而有针对性地编写检查这些错误的测试用例的方法。
测试策略
《孙子兵法》云“知己知彼,方能百战不殆”。对软件质量思考的不同角度,形成了不同的测试类型,不同类型对应不同的测试方法。要做好测试,需要对测试对象也就是我们所测试的系统有比较深刻的认识,有针对性地使用相应的测试方法才能达到尽可能全面测试的目的。
这里我们以 iLogtail 为例,分析如何根据其采集 Agent 的属性,制定相应的测试策略。iLogtail 作为一款近千万装机量的 Agent,其部署在用户环境中,缺乏足够的运维干预手段,相对于后端系统的可控性,对于质量提出了更高的要求。具体来说:
参数配置多:功能点众多,且都有较多的参数控制,组合数量更加庞大;众多的历史兼容考虑,进一步增加了复杂度。输入不受控:不仅iLogtail采集的数据是不受控的,而且由于其客户端软件的特点,配置文件也可能被错误配置,导致其非预期使用多。上下游依赖:iLogtail 作为通用的可观测数据采集管道,输入输出与第三方系统的交互多;配置入口多样。运行环境多样:Linux、Windows、主机、容器等;机器资源未知;网络环境复杂。环境动态变化:iLogtail所处的环境是动态变化的,需要考虑环境变化时采集器能否适应。动态的变化包括配置文件变化、容器增删变化、甚至是容器运行时升级等。针对以上特点,iLogtail 在测试环节应用了大量的测试方法,包括单元测试、功能测试、环境兼容测试、FailOver测试和性能测试等众多手段全面保障版本发布质量。
测试手段
单元测试单元测试的价值⼤家对于单测应该并不陌⽣,维基百科这样定义:在计算机编程中,单元测试(Unit Testing)⼜称为模块测试,是针对程序模块(软件设计的最⼩单位)来进⾏正确性检验的测试⼯作。
《Software Engineering at Google》一书总结了 Google 在测试⽅⾯的最佳实践。我们可以看到测试⾦字塔由三层构成,最底层就是单元测试、占⽐80%,是软件系统的地基。再往上是集成测试和端到端测试,分别占15%和5%。⾕歌推荐的这个⽐例是多年实践出来的结果,意在提升研发的效率(productivity)并提升对产品的信⼼(product confidence)。测试⾦字塔的核⼼理念之⼀就是“Unit Test First“,每个软件项⽬⾥的第⼀⾏测试应该是单测(TDD甚⾄认为第⼀⾏代码就应该是单测),⽽且⼀个项⽬⾥占⽐最⾼的测试也应该是单测。
出自《Software Engineering at Google》
相反的,只关注⽤户视⻆的端到端测试、⼤量依赖QA测试都会产⽣如下图所示的反⾯模式。很不幸,这也是在过去的测试体系影响下最常⻅的模式。冰激凌筒模式下,测试通常运⾏缓慢、不可靠、难以使⽤。最可怕的是由于缺失基础的单元测试,代码中往往隐藏着“负负得正”的情况,也会让项⽬变得⾮常难维护,很难做⼤的改动。
出自《Software Engineering at Google》
有效的单元测试有助于尽早在尽量小的范围内暴露错误。其优点主要表现在:
本地调测方便,执行速度快,改动后更快的反馈,有助于尽早的发现问题。活文档,提升代码⾃解释性。支撑重构,发布迭代更有信心和底气。测试驱动开发 TDD既然单元测试有这么多好处,那如何在开发实践中应用呢?首先我们需要明确单元测试与编码开发的关系是什么?你是否习惯于这样说“编码完成了,正在补UT”?
回答这个问题前,我们先回顾下测试驱动开发的理念。测试驱动开发 TDD 是一种不同于传统软件开发流程的新型开发方法。它要求测试先行,在编写某个功能的代码之前先编写测试代码,然后只编写使测试通过的功能代码,通过测试来推动整个开发的进行。简单来说,TDD 就是一个红-绿-蓝的循环。
由此可见,单元测试与编码应该是一个循环迭代的过程。众所周知,高质量代码的一个核心评判维度是其可测试性。采用测试驱动开发策略,即在编写实际功能代码之前先设计并实施单元测试,是促进产生易测试代码的一种高效实践。此方法论不仅要求开发者预先设想代码的行为规范,还促进了模块化设计,从而使得代码结构更加清晰,职责明确,易于被独立验证和维护。
端到端 E2E 测试E2E测试是一种全面的测试方法,可以模拟真实用户场景,验证整个系统的功能和性能。它能够发现单元测试难以发现的问题,如全链路管控、数据流转、性能瓶颈等。
在 iLogtail 中借鉴单元测试的思想,设计了一套 E2E 测试框架。整体流程主要分为4个部分,分别是Setup、Trigger、Verify 和 Cleanup。
Setup 部分负责模拟测试的环境,可以模拟主机和容器环境。Trigger 部分负责生成待采集的数据,如正则日志、Json日志、Metric、Trace等。Verify 部分负责验证数据采集的正确性,正确性不仅包括数据的数量正确,也包括内容的正确性,如Tag、解析字段是否正确,同时 Verify 部分还可以收集性能相关的数据,如日志采集延时、CPU、MEM占用率等。Cleanup 负责清理当前测试用例的上下文。该 E2E 测试采用行为驱动开发(Behavior-Driven Development)的设计思路,通过定义一系列测试行为,并通过配置文件的方式来描述测试场景,从而实现对插件的集成测试。测试引擎会根据配置文件中的内容,正则匹配对应的函数,并解析配置文件中的参数,传递给对应的函数。从而完成自动创建测试环境、启动 iLogtail、触发日志生成、验证日志内容等一系列操作,最终输出测试报告。
下面是 iLogtail 正则解析 E2E 测试的用例,通过 Given-When-Then 行为驱动开发的思想,以结构化方式描述测试场景。给定初始条件(Given)、执行操作(When)和验证结果(Then),使测试更加清晰和易于理解。
@inputFeature: input file Test input file @e2e @host Scenario: TestInputFileWithRegexSingle Given {host} environment Given subcribe data from {sls} with config """ """ Given {regex_single} local config as below """ enable: true inputs: - Type: input_file FilePaths: - /tmp/ilogtail/**/regex_single.log* processors: - Type: processor_parse_regex_native SourceKey: content Regex: (\S+)\s(\w+):(\d+)\s(\S+)\s-\s\[([^]]+)]\s"(\w+)\s(\S+)\s([^"]+)"\s(\d+)\s(\d+)\s"([^"]+)"\s(.*) Keys: - mark - file - logNo - ip - time - method - url - http - status - size - userAgent - msg """ When generate {100} regex logs to file {/tmp/ilogtail/regex_single.log}, with interval {100}ms Then there is {100} logs Then the log fields match regex singlFailOver测试iLogtail 作为可观测基础设施,一直稳定服务阿里集团、蚂蚁集团以及众多公有云上的企业客户,目前已经有千万级的安装量,每天采集数十 PB 可观测数据。能发展到今天的规模,也离不开背后对稳定性建设的持续投入,而 FailOver 测试是其中关键的一环。
FailOver 意思是“故障转移,失败自动切换”。FailOver 测试是一种验证系统在发生故障时能否自动切换到备用资源,并保持服务连续性的测试方法。要做好 FailOver 测试,需要面向错误场景测试设计。对于任何一种系统,最不稳定的因素往往是外部的交互。因此第一步需要梳理外部依赖,以及不可用时对新接入和存量数据采集的影响,并按照针对性地进行测试设计。
FailOver 测试过程中,观测点也很重要,并且需要有足够的可观测手段辅助支撑结果验证。iLogtail 作为一个多租的采集器,重点需要关注:
异常态:多租隔离,例如单目标库网络异常不能影响全局运行;资源控制,避免异常状态反复重试消耗大量资源。恢复态:聚焦异常恢复能力,除了恢复后的稳定性,还需要保证故障期间数据的At Least Once。兼容性测试iLogtail 作为可观测采集 Agent,需要兼容所有主流 Linux 和 Windows 操作系统,涵盖 X86-64 和 ARM 架构,比如CentOS 5之后就有 40+ 版本。鉴于此庞大异构环境,确保运行时的全面兼容性无疑是一项重大的技术挑战。为此,研发了一套兼容性测试框架与 E2E 框架融合,实现自动购买多规格 ECS 实例用于运行时兼容测试。
Configserver 作为 iLogtail 管控服务端,承载了所有的配置管理职责。鉴于配置参数较多,涉及比较复杂的处理逻辑,很容易造成不兼容变更。为此,引入了一套服务端流量镜像 Diff 测试能力,通过将线上配置镜像到预发环境,并基于线上访问日志还原请求,可以彻底杜绝配置转换兼容性问题。
性能测试性能测试是确保软件系统在实际部署环境中稳定运行、满足用户需求和处理预期负载能力的关键环节。iLogtail 作为数据密集型的采集器,性能是关键的考量因素,如何保证在日常开发过程中不引入性能下降是比较大的挑战。
为此,研发了一套性能看护框架,围绕核心采集场景,进行 Commit 点的 Benchmark 测试,做到当日识别性能恶化风险。
论测试自动化
随着应用规模和复杂度的不断增加,单纯依靠人工测试已经难以应对。特别是面对 iLogtail 众多的功能点,手动测试更是显得力不从心。
自动化测试带来如下收益:
可以快速、频繁地执行大量测试用例,极大提高了测试效率和覆盖率。与开发流程融合,能及时发现代码变更引入的问题,为快速迭代和频繁发布提供了基础。自动化还能减少人为错误,提供可重复的测试结果。自动化测试虽然有很多优点,但也存在一些局限性,自动化测试的初始投入较大,需要时间和资源来开发和维护测试脚本。因此,自动化测试通过平台化思维,追求长期价值。
iLogtail 通过一套 E2E 框架统一了开源版、商业版测试,支持 Docker-Compose、ECS、ACK等多种运行环境,并且跟持续集成相融合,构建每日回归。
开源分支代码由用户通过 PR 提交到开源仓库。PR 发起后 Github Action 自动触发,执行编译、静态检查、UT、FT等合入门槛测试。只有都通过的代码才可以合并入库。商业分支:商业版测试流水线通过阿里内部的可视化 CI 平台编排内部代码仓库会每日自动同步开源库的代码,并触发编译、静态检查、安全检测、UT等。E2E 测试分成两个阶段:执行门槛测试、全量回归测试。门槛用例专注于基本场景,尽可能早地发现问题;全量回归测试,聚焦动态变化及异常场景。代码重构与测试体系建设
正如《代码整洁之道 -- 告别码农,做一个有思想的程序员!》中提到的,软件开发随着时间的推移引入更多特性,软件的复杂度会变得越来越高。而造成代码腐化的主要原因有:
业务逻辑复杂:业务逻辑复杂的代码,如果初期没有很好的设计导致不易扩展;后期又不断引入新的特性,加剧了代码的复杂度。开发阶段时间紧张:为了快速开发使用重复或结构差的代码来实现,以及后面再补的思维。缺乏代码重构:当代码不断变得不易维护时,开发人员没有进行及时意识到代码的坏味道,并进行有效的重构,导致代码越来越复杂。缺乏单元测试和集成测试:导致历史代码没人敢动,只能维持现状。面对代码坏味道,往往需要通过代码重构,在不改变软件可观察行为的前提下,提升代码的扩展性和可理解性,降低维护成本。代码重构,作为提升软件代码质量和维护性的关键实践活动,并非仅依赖个人经验的随意调整,而是一个需要严谨方法论指导的技术过程。即便是拥有多年经验的老手,如果仅仅依靠过往经验进行重构,也可能会忽略新的问题域特性,或是低估系统复杂度的增长,最终难免会在某些时刻遭遇意想不到的“雷区”。
因此,在进行代码重构时,采取“测试先行”的策略至关重要。这意味着在修改任何现有代码之前,首先编写或更新相应的测试用例来确保现有功能的正确性。这样做的好处是显而易见的:一方面,坚实的测试套件如同安全网,让开发者在重构过程中有底气大胆调整架构和逻辑,确信改动不会无声无息地引入错误;另一方面,测试先行鼓励模块化和解耦设计,因为易于测试的代码往往是结构清晰、接口明确的代码,这本身也是高质量代码的标志。
结束语:为了更美好的生活,请写好测试用例并做好自动化!