GitLab CI/CD pipeline和DevOps之道系列文章(二)

182 阅读14分钟

# GitLab CI/CD pipeline和DevOps之道系列文章(一)我们继续了解上节的内容。 通常来说测试可以帮我们检查的涉及有效和无效数据行为的正确性。同样的代码的单元测试能够在更深层次的帮我们检测代码的逻辑。

在大多数情况下,我们只需要进行是一个单独的方法或函数的单元测试。例如,您可能想测试一个名为alphabetize的函数,它接受任意数量的字符串作为输入,并按字母顺序返回这些相同的字符串。为了测试这个函数,您可能会使用一种称为单元测试的测试方法。一个单元是一个单独的函数。您可以拥有几个单元测试的集合,它们都以不同的方式涵盖该函数:

  • 有些可能涵盖happy path。例如,它们可以将字符串"dog","cat"和"mouse"作为输入。
  • 有些可能是corner case的情况。例如,它们可以将函数的输入传递一个空字符串、仅由数字组成的字符串或已经按字母顺序排列的字符串。
  • 有些可能涵盖异常的路径。例如,它们可以将函数传递给意外的数据类型,如布尔值,而不是预期的字符串数据类型。

为了验证更大范围的代码块的行为,我们可以使用集成测试。这些测试并不关注单个函数如何实现,而是关注一系列有业务逻辑的函数之间的交互方式。例如,想象一下您的货币转换应用程序有四个函数:

  • get_input,它以源货币、源金额和目标货币的形式从用户获取输入。
  • convert,它将源货币的金额转换为目标货币的正确金额。
  • print_output,告诉用户转换产生的目标货币金额。
  • main,这是您的应用程序的主入口点。当使用您的应用程序时,会调用该函数。它调用另外三个函数,并将每个函数的输出作为下一个函数的输入传递。

为了确保这些函数之间能够良好合作 -我们需要调用main的集成测试,而不是调用get_input,convert和print_output的单元测试。这让您以更高的抽象级别进行测试,更加接近实际用户行为。毕竟,用户不会孤立地调用get_input。相反,他们将调用main,然后main将调用其他三个函数,并协调在它们之间传递值。编写一个正常工作的函数很容易,但是让一组函数合作构建一个业务片段却很难。所以集成测试可以发现纯单元测试无法发现的这种问题。

测试人员通常将各种测试形成测试金字塔模型。根据这个模型,单元测试占据了金字塔的底部:它们是测试基础代码的部分,并且数量众多。集成测试占据了测试金字塔的中间部分:它们在抽象级别上比单元测试更高,并且数量较少。金字塔的顶部是第三类测试,我们接下来要谈论的 - E2E测试:

image.png 最后一种测试类型是端到端测试,它基本上完模拟用户的行为并以与用户相同的方式对软件进行测试。例如,如果用户通过输入源货币、该源货币的金额和目标货币与外汇应用程序进行交互,然后期望以目标货币的金额形式看到输出结果,那么用户测试就会按照这样的方式进行。这可能意味着使用应用程序的图形用户界面(GUI)通过点击按钮和在字段中输入值。或者可能意味着调用应用程序的 REST API 端点,传入输入值并检查输出值的结果。然而,它与应用程序进行交互,并以尽可能接近真实用户的方式进行。与单元测试和集成测试一样,用户测试可以包括正常路径测试、边界和特殊情况测试,以及不良路径测试,以覆盖软件规范描述的所有场景,以及测试设计人员可以构思出的任何其他场景。

到目前为止,我们已经解释了单元测试、集成测试和E2E测试的不同目的,但我们还没有描述另一个基本的区别。单元测试和集成测试几乎总是自动化的。也就是说,它们是测试其他计算机程序的计算机程序。虽然E2E测试在可能的情况下也是自动化的,但编写可靠、可重现与应用程序的 GUI 进行交互的测试存在许多困难,因此很多用户测试必须手动运行。由于在网页应用程序中加载时间、页面渲染不完整、缺少或加载不完全的 CSS 文件以及网络拥塞等因素的不可预测行为,测试网页应用程序变得非常困难。这意味着虽然软件开发团队经常尝试自动化进行网页应用程序的用户测试,但更常见的情况是它们最终采用自动化和手动混合的用户测试。正如你可能猜到的,手动用户测试在时间和测试人员士气方面都非常昂贵。

性能测试是另一方面的测试,应该对应用程序进行测试:它是否能够以足够快的速度执行用户期望的操作,避免用户感到沮丧?它是否满足开发人员在编码前可能收到的性能规格要求?它的性能是否明显好于或差于竞争对手的性能?这些都是性能测试旨在解答的问题。 性能测试是另一方面的测试,应该对应用程序进行测试:它是否能够以足够快的速度执行用户期望的操作,避免用户感到沮丧?它是否满足开发人员在编码前可能收到的性能规格要求?它的性能是否明显好于或差于竞争对手的性能?这些都是性能测试旨在解答的问题。

性能测试设计和执行起来通常非常困难。在评估应用程序运行速度时有许多变量需要考虑:

  • 在测试期间它应该运行在什么环境中?创建与生产环境完全相同的环境往往需要付出高昂代价,但您可以在测试环境中采取哪些折衷方案,而不会严重影响性能测试结果,比如采用类比的方式进行推算。
  • 您的性能测试应该使用什么输入值?根据应用程序的不同,某些输入值可能需要处理的时间比其他输入值长得多。
  • 如果您的应用程序是可配置的,您应该使用什么配置设置?如果没有大多数用户都使用的标准配置,这一点尤为重要。

即使你能够找出如何设计有用的性能测试,它们通常需要很长时间运行,并且在某些情况下会产生不一致的结果。这导致团队经常重新运行性能测试,从而导致更多时间的消耗。因此,性能测试是所有测试类型中最关键、也是最昂贵的。

负载测试是性能测试的近亲。性能测试确定软件执行一次操作(例如单个货币兑换、单个银行存款或单个算术问题)的速度,而负载测试确定您的应用程序处理许多用户同时与其交互的能力。负载测试面临与性能测试相似的设计困难,并且可能产生类似的不一致结果。由于需要模拟数百或数千个用户的方式,负载测试设置起来甚至更加昂贵。

soak浸泡测试当您的应用程序运行数小时或数天时,它是否分配了从不回收的内存?它是否通过过度记录消耗了大量的磁盘空间?它是否启动了永远不会关闭的后台进程?如果出现了这些资源"泄漏"问题,它可能会因内存、磁盘空间或专用CPU周期不足而失去性能甚至崩溃。通过soak浸泡测试浸泡测试,可以在一个较长的时间段内对软件进行测试,同时监控其稳定性和性能,以发现这些问题。很显然,浸泡测试在时间和硬件资源方面都非常昂贵。

fuzz模糊测试一种被低估但强大的测试,通常我们称之称为模糊测试。该方法向您的软件发送有效但奇怪的输入数据,以暴露传统功能测试可能忽略的错误。可以将其视为在模糊状态下进行的happy path测试。所以,与创建一个用户名为"Sam"的账号相比,试试一个由1000个字母组成的用户名。或者尝试创建一个完全由空格组成的用户名。或者在邮寄地址中包含克林贡字母Unicode字符。

模糊测试引入了强烈的随机性:它发送给软件的输入值要么是完全随机生成的,要么是已知对您的代码没有问题的输入值的随机排列。例如,如果您的代码将PDF文件转换为HTML文件,模糊测试可能首先发送稍微调整过的有效、易处理的PDF文件版本,然后开始要求您的软件将与PDF文件完全不相似的纯随机字符串进行转换。由于模糊测试在找到导致崩溃或其他错误的输入值之前可能发送成千上万个随机输入值,因此必须自动化执行模糊测试。这种测试手动执行起来太麻烦了。

静态代码分析另一种严格自动化的测试形式是静态代码分析。与我们讨论过的其他测试不同,静态代码分析在不执行代码的情况下检查源代码。它可以查找各种不同的问题,但总体而言,它会检查您是否符合公认的编码最佳实践和语言习惯。这些标准可以由您的团队、语言开发者本身或其他编程权威所制定。

例如,静态代码分析可能注意到您声明了一个变量,但从未给其分配值。或者它可能指出您为一个变量赋值,但后续从未引用过该变量。它可以识别出无法到达的代码、使用已知比其他可运行的但功能相等的模式更慢的编码模式的代码,或者在不正规的方式中使用空格的代码。这些做法可能不会导致您的代码出现错误,但可能使您的代码不如预期的那样易读、易维护或高效。

验证代码的更多挑战到目前为止,我们仅描述了验证代码行为、性能和质量的一些方式。但是,一旦您完成了所有这些不同类型的测试,您将面临如何解析、处理和报告结果的潜在困难问题。如果幸运的话,您的测试工具将会生成标准格式的报告,您可以将其整合到自动更新的仪表板中。但是您可能会发现自己使用至少一个无法适应常规报告结构的测试工具或框架,需要手动扫描、清理和转换为易于阅读和传播的格式。

我们已经提到性能测试特别需要反复运行。事实上,所有这些类型的测试都需要反复运行,以捕获回归错误或所谓的“闪烁”测试,这些测试有时通过,有时失败,取决于网络条件、服务器负载或无数其他不可预测的因素。这意味着如果是手动运行测试,将会是很大的负担,或者管理和触发自动化测试的负担,远远超出了我们表面上所看到的。如果您要反复运行测试,您需要确定何时以及多久运行测试,确保正确的硬件或测试环境在适当的时间可用,并且在条件变化或管理层要求更及时的结果时具有足够的灵活性。关键是,测试是困难、耗时且容易出错的,每当需要人为参与确保测试按照正确的方式在正确的时间进行时,所有这些困难都会被无限放大。

尽管我们刚刚说过测试通常应该运行一次,然后反复运行,但另一个相反的力量正在发挥作用。由于执行测试是昂贵且困难的,人们往往倾向于尽可能少地运行它们。这种倾向得到了一种常见的开发模型的鼓励,即开发人员构建一个功能(或有时是整个产品),然后将代码交给质量保证(QA)团队进行验证。这种严格的代码构建和代码测试之间的分离意味着在许多团队中,测试只在开发周期结束时运行 - 无论是在两周的冲刺结束时,还是在长达一年的项目结束时,或者之间的某个时间点。

不频繁或延迟测试的做法导致了一个巨大的问题:当开发人员将一大批代码交给测试时 - 数千行甚至数万行的代码,这些代码由不同的人在几周或几个月的时间内使用不同的编码风格和习惯开发 - 很难诊断测试发现的任何错误的根本原因。这反过来意味着很难修复这些错误。就像大捆干草比小捆更有效地隐藏了针一样,大批代码使得查找、理解和纠正其中包含的任何错误变得困难。开发团队在将代码交给 QA 团队之前等待的时间越长,这个问题就会变得越大。

这就是我们对功能测试、负载测试、soak测试、模糊测试和静态代码分析的快速调查。此外,我们还解释了运行所有这些不同类型测试所涉及的隐藏困难的一些方面。您可能想知道为什么我们要讨论测试。原因就在于,了解测试的挑战 - 了解有多少种方式可以验证您的代码,不同形式的测试有多重要,设置测试环境需要多少时间,手动运行不可自动化的E2E测试有多麻烦,处理和报告结果有多棘手,以及在一个庞大的代码包中找到和修复潜藏的错误有多困难。 了解在 DevOps 出现之前软件开发是多么困难的一部分。当您在本西勒文章中了解到 GitLab CI/CD 流水线如何简化运行不同类型测试和查看结果的过程时,当您理解早期和经常运行测试使问题更容易检测和修复更便宜时,您可以回顾这些繁琐的测试程序,并对曾经在 GitLab 不存在之前,开发人员不得不应对软件开发生命周期中的这一部分感到同情。在 GitLab 时代,生活变得更好了! 明天继续把第一章内容和大家分享完毕。