后台自动化测试与持续部署实践

 公司动态     |      2022-09-29 03:55:00    |    来源:雷竞技官方网站下载     作者:雷竞技app官方版下载

  随着 DevOps 研发模式思想的普及,“测试左移”、“开发负责质量”等理念也开始深入各业务团队。本文以一个实际项目( LogReplay )的 DevOps 实践为例,介绍如何通过可测性提升、自动化测试、持续集成和持续部署流程,最终实现后台微服务的高质量、持续、自动化部署。

  测试左移是 DevOps 研发模式中开发全面负责质量的核心环节之一,而测试左移的一个重要手段,就是在开发过程中的各环节快速执行大量有效的自动化测试用例,从而尽早地发现得到质量反馈,发现潜在的代码问题。(详细开展参考 2.自动化测试)

  软件可测试性对软件研发和质量保障有着至关重要的作用,可测试性是实现高质量、高效率交付的基础。可测试性差,会直接增加测试成本,让测试的结果验证变得困难,进而会让开发者不愿意做测试,或者让测试活动延迟发生。所以在全面开展自动化测试之前,需要提升软件的可测试性。(参考 1.可测性提升)

  通过大量的自动化测试,我们可以取代低效的手工测试验证。将测试合理的配置到 CI/CD 流水线中,从而可以在提交代码后,立即进行测试、构建制品,再通过一系列环境的测试验证(在上一个环境测试通过后,才能进入下一个环境),最终将制品自动发布上线.持续集成,持续部署)

  可测性,简单来说,就是指一个软件系统能够被测试的难易程度。可测试性差,会直接增加测试成本,让测试的结果验证变得困难,进而会让工程师不愿意做测试,或者让测试活动延迟发生。

  接口测试缺乏详细的设计文档:接口测试如果没有设计契约文档作为衡量测试结果的依据,就会造成测试沟通成本高, 无法有效开展结果验证,开发和测试来回扯皮的尴尬窘境。即使有了文档,还必须保持文档能够及时更新,否则会造成误导。

  构建 Mock 服务的成本过高:微服务架构下,如果构建 Mock 服务的难度和成本过高,会直接造成不可测或者测试成本过高。

  接口调用的结果验证困难:接口成功调用后,判断接口行为是否符合预期的验证点难易获取。

  接口调用不具有幂等性:接口内部处理逻辑依赖与未决因素,比如时间、不可控输入、后台批处理 job、随机变量等,破坏接口调用的幂等性。

  接口参数设计过于复杂,暴露了很多不必要的参数:很多内部参数不应该在接口参数上暴露出来,这些参数应该做到无感知,需要保持接口设计的简单性。

  使用定制化的私有协议:非标的私有化协议会提升测试的难度,通用类的工具无法直接使用。

  函数功能的多样性:一个函数如果颗粒度太大,同时实现了好几个功能,会大大提升测试的难度,一来这是因为功能多必然入参也多,测试的时候参数初始化难度就会变大,二来结果验证的关注点也会同时变多,容易出现更多的组合验证,严重的时候会出现组合爆炸。

  代码依赖关系复杂:被测代码中依赖了外部系统或者不可控组件,比如,需要依赖第三方服务、网络通信、数据库等。

  代码可读性差:代码使用“奇技淫巧”,造成可读性差,同时又缺乏必要的注释说明。

  重复代码多:重复代码意味着重复逻辑,如果有改动,各个重复逻辑都需要被测试到,测试成本高。

  代码的圈复杂度(Cyclomatic Complexity)过高:圈复杂度过高的代码往往测试成本很高。

  设计上钩子和注入点缺失:没有预留钩子或者注入点,后期调试和定位问题的扩展能力变差。

  可观测性是指能否容易地观察程序的行为、输入和输出,一般是指系统内的重要状态、信息可通过一定手段由外部获得的难易程度。

  任何一项操作或输入都应该有预期的、明确的响应或输出,而且这个响应或者输出必须是可见及并且是可查询的,“不可见”和”不可查询“就意味着“不可发现”,可观测性就差,进而影响可测试性。

  “可见”的前提是输出,提高可观测性就应该多多输出,包括分级的事件日志(Logging)、调用链路追踪信息(Tracing)、各种聚合指标(Metrics),同时也应该提供各类可测试性接口获取内部信息以及系统内部自检信息的上报,以确保影响程序行为的因素可见。另外,有问题的输出要易于识别,无论通过日志自动分析还是界面高亮显示的方式,要能有助于发现。

  当前服务的下游依赖服务越多,具体的失败点也就会越多,直接下游服务数量会增加失败点的常数量级(加法关系),而间接下游服务的数量会增加失败点的几何量级(乘数关系)。我们不可能把下游暴露的错误原原本本地透传到客户端去,因为客户端不见得能理解所有错误,或者不一定根据不同错误有不同的应对措施,因此状态码必须收敛。

  上游不必关心具体失败点(即端到端调用的返回信息可能不足以定位失败点),但必须向上游抛出。将失败完全内部消化,只会让上游调用者不知道请求是否成功,或者无法确认应对操作。

  在 trpc(腾讯内部一款服务框架)服务中,统一的错误由错误码 code 和错误描述 msg 组成,这与 go 语言常规的 error 只有一个字符串不是很匹配。因此,我们使用 trpc 框架封装的 errs.New 将状态码与状态消息一并返回(如果下游未用 errs.New 返回错误,上游拿到的状态码是 999)

  在排查错误时需要找到具体失败点,记录失败点的手段有多种:可以使用日志系统记录下来,可以在相同的错误码中使用不同的错误信息,也可以在全链路追踪中埋点。

  其中,接入分布式日志收集,可以最大限度保留定位信息。我们可以使用腾讯内部日志工具上报到鹰眼(腾讯内部工具),也可以使用智研(腾讯内部工具)。接入智研只需要在 trpc_go.yaml(trpc 服务的配置文件)里配置,查看日志的体验基本相当于 kibana。

  状态码和状态消息是面向客户的,拿着它们去找失败点可能会定位精度不足。全链路追踪非常有价值,任何现代的后端系统都应该接入一套 OpenTelemetry 的实现,使用 OpenTelemetry 的好处是其协议具有通用性,可以很好地被各种工具支持。每一个严肃的业务开发都有必要了解这一块的知识,当你需要排查一个线上问题而无从下手时就会深有体会。

  腾讯内部天机阁工具可以作为 OpenTelemetry 的后端,我们将所有的服务接入了天机阁。接入全链路追踪后,在接口测试和端到端测试时,使用统一的格式将 Trace ID 打印到 test log 中,一旦测试失败,就可以拿着 Trace ID 去快速定位失败点。

  可理解性是指被测系统的信息获取是否容易,信息本身是否完备,并且易于理解。比如被测对象是否有说明文档,并且文档本身可读性以及及时性都有保证。常见的可理解性包含以下这些方面:

  提供用户文档(使用手册等)、工程师文档(设计文档等)、程序资源(源代码、代码注释等)以及质量信息(测试报告等)

  可控制性是指能否容易地控制程序的行为、输入和输出,是否可以将被测系统的状态控制到测试条件的要求。一般来讲,可控制性好的系统一定更容易被测试,也更容易实现自动化测试。可控制性一般体现在以下各个方面:

  在业务层面,业务流程和业务场景应该易分解,尽可能实现分段控制与验证。对于复杂的业务流程需合理设定分解点,在测试时能够对其进行分解。

  在架构层面,应采用模块化设计,各模块之间支持独立部署与测试,具有良好的可隔离性,便于构造 Mock 环境来模拟依赖。

  在数据层面,测试数据也需要可控制性,能够低成本构建多样性的测试数据,以满足不同测试场景的要求。

  在技术实现层面,可控制性的实现手段涉及很多方面,比如提供适当的手段在系统外部直接或间接的控制系统的状态及变量、在系统外部实现方便的接口调用、私有函数以及内部变量的外部访问能力、运行时的可注入能力、轻量级的插桩能力、使用 AOP(Aspect Oriented Programming)、trpc-filter(trpc 服务的插件特性)等实现更好的可控制性等。

  为了提升中间件的可隔离性及更好的构建测试数据,我们对中间件做了下述几项治理工作:

  微服务架构下,如果还是使用固定的 ip:port 访问中间件,将难以灵活应对中间件扩缩容,也很难使用集群式管理。因此,寻址方式应该统一使用名字服务,统一通过 namespace+env 方式进行寻址,无需再为各个环境单独配置 ip:port。

  b. 通过名字服务寻址,不同环境用同一个名字 tap.db,再通过 namespace/env 到北极星(腾讯内部的统一服务发现和治理平台)寻址不同环境的地址,即可访问;

  使用腾讯内部统一的中间件管理模块 trpc-database 作为中间件的访问 client,有几个优势:

  b. 每种中间件的访问,统一使用一种 client,避免使用了一些个人 github 名下的 client 实现,因功能或使用上的差异导致写代码时容易出 bug(如);

  c. 基于 trpc 框架,可以使用 007(腾讯内部的服务监控平台)、天机阁等能力,提升可观测性,更方便监控;

  d. 可以使用 trpc-filter 对流量作额外操作,比如修改路由等,操控更灵活。

  中间件环境分为正式环境 Production、基线环境 Development 和自动化测试环境,手工测试和体验使用 Development 环境的中间件,自动化测试则使用自动化测试环境。不同环境,尤其是生产环境与诸测试环境之间,需要使用不同的实例作物理隔离,这是避免测试行为影响生产环境的必要手段。

  除了中间件的治理,其他我们在可控制性上的实践目前也不够多,后面有更多经验时再进行更多的分享。

  三种测试从上到下实施的容易程度递增,但是测试效果递减。端到端测试最费时费力,但是通过测试后我们对系统最有信心。单元测试最容易实施,效率也最高,但是测试后不能保证整个系统没有问题。

  我们当然希望能“简单点”,用一种测试搞定所有的事情,但实际的实践来看,目前还没有这种“银弹”方案,我们仍然需要组合式的开展。

  然而问题是,什么时候该写单元测试、什么时候该写接口测试或端到端测试?又各需要写多少呢?

  我们的实践中,主要有手工编写单元测试和借助 TestOne 单测辅助工具自动生成单测用例。手工写单元测试的方法的文章比较多,观点也非常多,单元测试代码怎么写、也有非常多的教程介绍,推荐使用 PCG Testability 认证培训中所介绍的单元测试的 5 个方法(聚焦行为、显式依赖、封装细节、职责单一、Readability),这里不再细述。

  单元测试在腾讯内部的普及时间并不长,我们存在一些没有单测用例或者单测用例较少的存量代码库,这些代码库一旦由于业务需要发生逻辑变更,缺少快速回归的自动化测试手段。对于这些问题,我们使用了 TestOne 单测辅助工具,来协助我们提高编写单测的效率和质量,以及提升存量代码库的自动化率。

  对于 Logreplay 不断迭代的需求中的增量代码,可以使用 TestOne 单测辅助工具 的脚手架功能快速生成单测模版,相较于 gotests 生成的模版,工具提供了依赖分析、调用链分析、mock 生成、指针类型断言分析等功能,可以起到测试数据简化、单测有效性提升、可读性提升,进而提升了单测编写的整体效率和质量。

  可以看到,测试数据按手写结构体的方式逐字段展开,方便开发同学填写不同的测试数据;变量 rsp 虽然在入参,但根据变量依赖分析判断其在方法中被写入,会提示开发同学对其进行断言,设置 rspWhenReturn 字段,在调用结束后进行断言验证;通过方法调用链分析判断被测方法有 trpc 方法调用,自动生成了 mock trpc 接口调用的框架,方便开发同学直接构造 mock 场景;同时 //FIXME 注释会提示开发同学确认单测逻辑,避免造成辅助工具的滥用。

  对于 Logreplay 存量代码库,有的单测用例偏少,后续如果代码逻辑发生变更,缺少快速进行验证的单测回归测试手段。我们借助了 TestOne 单测辅助工具自动生成单测用例,为当前代码库快速建立质量保护网,为后续发生变更时进行验证提供了基本保障。

  目前 LogReplay 项目的单测用例已经覆盖了大部分的代码行,每天都会本地和流水线. 接口测试编写

  写接口测试,其实跟写单元测试类似,下面是一个简单的接口测试示例,测试服务 hello.world 的 某个 接口:

  当我们需要把接口测试用例放到 MR 阶段运行,更早的发现和修复问题,同时应该更大范围的开始编写接口测试用例时,很快就有了新的问题:

  数着日渐凋零的头发,我们开始分析失败的测试用例,发现失败的主要原因是用例质量不够高、依赖的服务变更了、用例并发运行时的数据冲突。怎么解决呢?一方面我们需要提升用例的编写质量,另一方面,我们需要解决因多个的测试环境共同使用相同的服务依赖及中间件依赖导致的不稳定问题,我们使用了 TestOne 提供的沙箱测试环境和 接口测试 SDK 的 Mock 能力来解决,同时对中间件进行了一些治理(参考 #1.2)。

  Mock 下游服务当下游/外部服务还没有开发完成时、或者下游/外部服务经常变更/不稳定、或者下游/外部服务返回的数据比较难触发时,我们在接口测试中使用 TestOne 提供的服务 Mock 能力。下面是以 LogReplay 项目的 1 个接口测试用例示例。

  当测试环境的 MySQL 等中间件不稳定、或者数据经常被修改、或者想要的数据比较难触发时,如希望 SQL select count(*) 返回 100W 等比较大的数据,我们在接口测试中使用 TestOne 提供的中间件 Mock 能力。下面是以 LogReplay 项目的 1 个接口测试用例示例。

  当我们想要快速的将用户的流量数据转换成接口测试,使用 TestOne 流量生成用例功能。流量生成用例可以录制线上用户流量,快速生成我们需要的接口测试用例。这里我们以 某个服务 服务为例

  当我们对新接口进行的接口调试时候,可以使用 TGuitar 的后台接口调试工具,对调试成功的数据可以自动生成接口用例,提升编写用例的效率、新增接口的用例覆盖以及对构造的接口数据的复用性。

  在开始大范围使用时,我们使用 TestOne 提供的接口覆盖率指标来制定接口测试的编写目标及策略:

  2.3.1. go test 直接运行不管是单测用例,还是接口测试、端到端测试用例,都可以直接用 go test 直接运行。

  接口测试场景,使用 TestOne Guitar CLI 运行,会自动创建稳定的沙箱测试环境、执行测试、销毁测试环境、生成测试报告。使用前需要先编写测试计划 TESTPLAN 文件:

  也可以使用 TestOne Guitar IDE 插件来运行测试,在 IDE 内,写测试代码的过程中,就可以非常方便的点击运行,不需要输入任何命令:

  当某次测试任务结束、发现有用例失败时,首先可以通过日志里展示的错误信息来定位问题;如果发现错误是下游返回的,则需要通过链路追踪查找最后一个报错的服务。

  面对用例失败,首先考虑的定位手段是用例执行过程的日志,我们可以在 TestOne Web 测试报告上看到。日志里的错误一般有三种类型:

  在日志中,最常见的是业务错误,偶尔也会出现框架错误。以请求 trpc 服务的错误为例,一般建议业务错误码>

  10000,1 ~ 200 以及 999 是框架错误码。几种比较常见的错误如下:

  被测服务接入天机阁后,在接口、集成、端到端测试用例运行中,TestOne 自动化测试工具会将天机阁 Trace ID 打印出来。当用例运行失败后,我们可以在测试报告中方便的找到 Trace ID 信息,点击可以跳转到天机阁页面,快速定位到用例失败的原因。

  如上例,从天机阁的调用链中,开发者可以很清晰地找到最后一个返回错误的服务是 A 服务的 a 接口,协议为 oidb,错误码为 11000。一般情况下,如果这个服务并非本次测试的被测对象,而且比较稳定,那么就有较大概率是环境问题,部署的服务版本不对。如果错误是被测服务直接返回的,我们优先检查被测服务是否有问题,再检查测试用例参数构造是否有错误。

  当一段时间内某些用例或服务频繁出错,我们以上游调用接口为维度来聚合下游错误,发现采样周期内下游频繁出现的问题,从而有针对性地进行处理修复:

  当服务发生了重构,重构前用例能通过,重构后却失败的时候,使用 TestOne 数据 diff 能力进行定位。下图是针对某条用例 2 次执行的协议请求(request)/应答(response),逐字段对比结构体中每个字段的差异。

  我们写了很多单测、接口测试、端到端测试用例,单测覆盖率、接口测试覆盖率都很高,但是依然还是有一些逻辑 bug 漏出,甚至有一些 bug 场景是有自动化测试覆盖的。这让我们开始审视我们自动化测试的有效性。

  在代码 CR 的过程中,我们通常喜欢只看业务逻辑代码,往往容易忽视对测试用例代码的 CR。测试代码跟业务逻辑代码一样,都非常重要,也都非常容易出问题,需要严格进行 CR。

  对线上缺陷问题、oncall 单等进行复盘,分析问题应发现阶段及未发现原因,自动化测试用例为什么没有覆盖到,并进一步分析用例覆盖缺失的原因,补充及修改用例。

  借助 TestOne 提供的用例有效性检测工具,来实现事前的有效性检测。

  单测有效性检测通常有两种方案,一种是静态代码扫描,一种是动态代码注入。静态扫描速度快,通常可用于发现一些简单的有效性问题,比如无断言、编译错误、断言不完整等;动态代码注入则通过在测试运行过程中动态修改原代码,模拟各种错误场景来检测用例的错误覆盖情况。其执行耗时较长,但能发现更为精细的有效性问题,比如边界值覆盖缺失、条件分支覆盖缺失等。

  给 TestOne 后台自动化工具提需求,支持用例执行统计功能,定制用例执行率、执行次数、失败分布等数据报表,定时 review 用例的执行情况,并进行优化调整(支持中)。

  接口测试和端到端测试实践的过程中,我们经常会遇到不稳定的用例( Flaky Test ):相同的测试用例,有时测试通过,有时又测试不通过。这样的测试用例可以理解为是不稳定、可靠度低的测试用例。造成用例不稳定的原因有很多种,比如测试代码本身的问题、测试框架的问题、被测系统及其依赖的软件库的问题等。如果有很多这种测试用例,我们将会对测试失去信心。

  在 LogReplay 项目的自动化实践中,我们使用 TestOne Flakiness 缓解方案提升端到端测试可靠性的运行方案:监控并记录测试用例的可靠性数值(flakiness 值),如果达到某个阈值,则认为这个用例不可靠,并自动移除该测试用例(不在关键路径中运行、或测试结果不作为关键路径是否成功的标志)。如下图所示:

  对微服务环境进行标准化治理,设置 Sandbox,Test,Staging,Canary ,Production 环境,标准化 CD 过程在各个环境中的升级。各个环境的作用:

  持续集成(Continuous Integration),将微服务的代码持续合并到主干,合并时通过构建和运行自动化测试来保证质量。

  LogReplay 项目的实践中,每次代码合入前都会触发代码 Review、单元测试、代码扫描、安全扫描、测试用例有效性扫描、接口测试,验证合入前的分支代码质量是否达标,只有所有的扫描及测试都通过,才允许合入代码。代码合入后,会再次触发单元测试、代码扫描、安全扫描、变异测试、接口测试,回归验证合入后的代码是否有质量问题。

  流水线中的单元测试、接口测试运行,使用的是 TestOne Guitar 插件,因本地运行时也使用了 Guitar CLI,流水线配制时非常简单清爽,只需要在拉取代码后,加上 Guitar 插件指定运行的 testplan 文件就行,无需再去配置覆盖率、产物解析、环境部署、质量红线等众多的插件。

  持续部署(Continuous Deployment),是 CI 的延续,持续、自动化的将微服务部署到测试和生产环境,不需要人工干预。

  LogReplay 项目的实践中,当主干有代码合入时,CD 流水线会自动触发拉取主干代码构建,先自动部署到 Test 环境、再到 Staging 环境、再到 Canary 环境,每个环境都设置了准入及准出条件,当所有条件都满足后,再按一定策略自动部署到生产环境,一旦出现问题(灰度策略中介绍),会自动进行回滚,全过程无需人工干预。

  在 LogReplay 项目当前的实践中,我们基本实现了微服务业务代码变更的持续部署能力:通过大量的自动化测试及完善的 CICD 流水线,代码 MR 到主干后,无需人工干预,全自动部署到生成环境中,部署过程中如果有问题将自动回滚。

  不同类型的业务场景和需求差别较大,自动化测试与持续部署的方式及思路也不尽相同,本文只是我们的“一家之言”,并不一定适用于其他的业务场景。但,我们追求的目标,应该是类似的,都是希望能“质量更高”、“速度更快”,而实现这两个目标,都离不开自动化测试、自动化部署。回到本文的标题,希望有越来越多的业务一起探索实践、及分享后台自动化测试与持续部署实践,也随时欢迎一起交流。