回归——是什么、为什么发生以及如何测试

作者: IT Sectr 发布日期: 2026-07-30 阅读时间: 7 分钟

回归 — 是在对代码进行更改后出现的错误,尽管相同的功能之前运行正常。回归意味着新的更改“破坏了”之前已经编写和测试过的内容。这是开发中最常见和最危险的问题之一:在修复一个错误时,开发人员可能无意中破坏其他三个功能。根据Capers Jones Software Engineering 2023的数据,回归错误的平均密度是每100行修改的代码出现1-3个。我们分析回归的原因、检测方法和预防策略。

要点

  • 回归 — 对先前工作的代码进行更改后出现的错误
  • 主要原因 — 更改的副作用:代码通过隐式依赖关系连接
  • 单元测试和回归测试 — 检测回归的主要工具
  • 手动回归测试不可扩展 — 需要自动化
  • 带有自动化测试的CI/CD管道在回归进入生产环境之前捕获它们

开发中的回归是什么

回归 — 是指先前版本中正常工作的功能在进行更改后停止工作的情况。更改可以是任何内容:修复错误、添加新功能、重构、更新库甚至更改配置。回归是稳定性的主要敌人:每次更改都有可能破坏已经检查和发布的内容。

这个术语来自测试:回归测试 — 是在每次更改后重新运行现有测试。如果先前通过的测试失败——意味着发生了回归。从更广泛的意义上说,回归不仅仅是测试失败,还包括用户或QA观察到的任何行为恶化。根据Tricentis State of Testing 2023的数据,回归占生产环境中发现的所有错误的35-45%。

回归与普通错误的区别在于时间上下文:错误可能一直存在,而回归总是更改的结果。这是一个重要的区别,因为寻找回归原因从分析更改开始:在“正常工作”和“停止工作”之间发生了什么变化。Git bisect — 是查找导致回归的提交的标准工具。

回归类型和示例

局部回归 — 模块A中的更改破坏了同一模块A中的功能。示例:开发人员重写了排序函数,该函数停止正确处理空数组。局部回归最容易检测和修复,因为原因和结果很接近。

远程回归 — 模块A中的更改破坏了模块B中的功能,后者不直接通过代码关联,而是通过数据或时间关联。示例:“用户”模块中的数据库模式更改破坏了使用相同表的“分析”模块中的报告。远程回归是最阴险的:开发人员不会怀疑他的更改会影响另一个模块。

副作用回归 — 副作用(日志记录、缓存、发送通知)的更改破坏了预期行为。示例:开发人员添加了缓存以加速工作,但由于过期缓存,用户看到过时的数据。副作用回归很难通过自动化测试捕获,因为副作用通常没有被测试覆盖。

性能回归 — 代码在功能上继续正常工作,但比以前更慢。示例:新的加密算法产生相同的结果,但执行时间从2毫秒增加到200毫秒。性能回归不会被普通单元测试检测到——需要基准测试和分析。

回归类型示例检测方法
局部排序损坏单元测试
远程数据库模式变化集成测试
副作用过期缓存E2E测试
性能响应变慢基准测试

为什么会出现回归

第一个原因 — 代码耦合(coupling)。模块之间的依赖越强,一个模块中的更改导致另一个模块中回归的可能性就越高。经典的反模式:God Object(做所有事情的对象)、Shotgun Surgery(更改一个需要数十个地方的调整)、Circular Dependency。减少耦合是架构的任务:SOLID原则、依赖注入、六边形架构。

第二个原因 — 更改的功能缺乏测试。如果代码没有被测试覆盖,开发人员只能从QA或用户那里了解到回归。根据Google Testing Blog,测试覆盖率>75%的项目比覆盖率<25%的项目回归少5倍。TDD(测试驱动开发)保证测试是在代码之前编写的,而不是“等有时间的时候”。

第三个原因 — 人为因素。开发人员不知道相邻功能的存在,不理解所有依赖关系,或者只是匆忙。原因——代码库知识共享不足。解决方案:来自其他模块的开发人员参与代码审查、结对编程、架构文档化。项目的公交因子与记录的架构决策数量成反比。

回归测试及其作用

回归测试 — 是在每次更改后重新运行现有测试以检查旧功能是否被破坏的过程。这是保证新更改不会干扰现有代码工作的唯一方法。没有回归测试,每次发布都是一场彩票:开发人员希望没有破坏任何东西,但无法确认。

手动回归测试 — 最昂贵和低效的方法。随着项目的增长,回归测试场景的数量线性增长,手动执行的时间呈指数增长。经过2-3年的开发,手动回归测试可能需要2-3周,这使得频繁发布变得不可能。唯一的出路是自动化。

自动化回归测试根据测试金字塔分为不同的级别:

  • 单元测试 — 快速、隔离、覆盖单独的函数和方法
  • 集成测试 — 检查模块、数据库、外部服务之间的交互
  • E2E测试 — 通过UI或API检查完整的用户场景
  • 快照测试 — 比较组件的当前输出与参考

根据Google Testing Blog,最佳比例是:70%单元测试、20%集成测试、10%E2E测试。偏离这个比例会降低回归测试的效果:过多的E2E测试会减慢管道,缺乏单元测试会让微错误被忽视。

回归测试自动化策略

第一种策略 — Full Regression(完整回归)。项目的所有测试都运行。最可靠但最慢的方法。适用于小型项目(最多10,000个测试,运行时间<30分钟)。对于大型项目,完整回归可能需要数小时,这使得CI/CD管道不切实际。

第二种策略 — Selective Regression(选择性回归)。只运行与更改代码相关的测试。使用代码的依赖关系图来确定关联。工具:Bazel(Google)、Nx(JavaScript)、sbt(Scala)。选择性回归节省60-80%的运行时间,但需要精确构建依赖关系图——错误会导致遗漏回归。

第三种策略 — Prioritized Regression(优先级回归)。所有测试按优先级排序:critical path(最重要的用户场景)、high risk(有错误历史的代码)、changed code(受更改影响的代码)。首先运行最高优先级的测试——如果通过,开发人员将获得快速反馈。限时运行:在10分钟内检查关键测试,其余在后台进行。

如何在项目中防止回归

第一步也是最重要的一步 — 编写测试的文化。每次更改都应该附带一个测试来检查更改是否有效,以及一个测试来检查没有破坏任何东西。TDD(测试驱动开发)提供最佳结果:开发人员首先编写失败的测试,然后编写使其通过的代码。这保证了测试在代码之前存在。

第二步 — 带有强制测试运行的CI/CD管道。在所有测试通过之前,pull request不能合并。不能因为紧急而“跳过”测试——紧急更改会通过加速但强制性的测试集。根据Google DevOps Research,具有强制CI/CD的团队在生产中的回归少3倍。

第三步 — 生产环境监控。即使是最好的测试也不保证100%防止回归。可观测性工具(Sentry、Datadog、New Relic)应该在每次部署后跟踪关键指标:错误率、延迟、吞吐量。超过阈值时自动回滚(rollback)——如果回归仍然进入生产环境的最后一道安全网。

第四步 — 带有回归思维的代码审查。审查者应该问:“这个更改可能会破坏哪些其他模块?”。仅仅检查代码的正确性是不够的——还需要检查它不会干扰相邻功能。代码审查的检查表应该包括“检查相邻模块中的回归”这一项。

常见问题

回归与普通错误有什么区别?

回归 —— 是一个之前不存在的错误。普通错误可能从功能创建时就存在。回归总是与特定的更改相关——这允许使用git bisect来查找原因。

如何快速找到回归的原因?

使用git bisect:指定一切正常工作的提交和出问题的提交。Git将在历史记录中执行二分查找,找到导致回归的提交。即使在有数千次提交的大型项目中也能工作。

需要多少测试来防止回归?

没有确切的数字,但有一个经验法则:关键用户流程的覆盖率应为100%,所有功能的覆盖率至少为70%。质量比数量更重要:检查边缘情况的测试比happy path上的十个测试更有价值。

回归可能是由基础设施而不是代码引起的吗?

是的,这被称为基础设施回归(infrastructure regression)。更新操作系统、数据库版本、SSL证书或Web服务器配置可能会破坏正在工作的代码。IaC(基础设施即代码)和基础设施测试(Test Kitchen、Terratest)有助于捕获此类回归。

如果团队从未编写过回归测试,如何说服他们编写?

一个关键用户流程开始。为最重要的场景(登录、下订单)编写自动化测试。在演示中展示测试如何捕获回归。当团队看到好处时——逐步实施测试,扩大覆盖范围。

总结

  • 回归 — 更改先前工作的代码后出现的错误
  • 四种回归类型:局部、远程、副作用和性能
  • 主要原因——代码耦合、缺乏测试和人为因素
  • 回归测试——保持稳定性的必要过程
  • 通过测试金字塔(70/20/10)自动化回归测试
  • 带有强制测试运行的CI/CD阻止回归进入
  • Git bisect——查找导致回归的提交的标准工具

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读