行为驱动开发

在第 7 章 "测试网络应用程序" 中,我们已经介绍了可以使用的各种工具,例如自动测试,以确保应用程序不出现错误。我们介绍了什么是单元测试,以及它们如何帮助我们实现目标,但这还远远不够。在本节中,我们将介绍创建真实世界应用程序的过程,单元测试是如何不够的,以及在这个生命周期中我们还可以使用哪些其他技术来完成我们的任务—​在本例中就是行为测试。

持续集成简介

独自开发一个小型网络应用程序,与作为一个由开发人员、经理、营销人员等组成的大团队的一员,围绕同一个大型网络应用程序开展工作,两者之间存在巨大差异。开发一个有成千上万或数百万用户使用的应用程序有一个明显的风险:如果你搞砸了,就会有大量不满意的用户受到影响,这可能会导致销售额下降、合作关系终止等等。

从这种情况可以想象,如果要在生产中改变任何东西,人们都会感到害怕。在这样做之前,他们会确保一切正常运行。因此,围绕影响生产中网络应用程序的所有更改,总是有一个繁重的过程,包括大量的各种测试。

有些人认为,只要减少向生产环境部署的次数,就能降低失败的风险。

现在,试想一下,一次性发布两三个月的代码修改成果,却在生产中出现了神秘的故障:你知道该从哪里开始寻找问题的原因吗?如果您的团队有足够的能力发布完美的版本,但最终结果却不符合市场需求,那该怎么办?最终可能会浪费几个月的时间!

尽管有各种不同的方法,而且并非所有公司都使用这些方法,但让我们试着描述一下最近几年最著名的方法之一:持续集成(CI)。其理念是经常集成小块工作,而不是偶尔集成大块工作。当然,发布仍然是系统中的一个制约因素,这意味着需要花费大量的时间和资源。CI 会尽可能地将这一过程自动化,从而减少您需要投入的时间和资源。这种方法有以下巨大优势:

  • 发布不需要花很长时间,也不需要整个团队专注于发布,因为发布是自动完成的。

  • 你可以根据变更逐一发布。如果出现故障,你可以清楚地知道改动是什么,以及从哪里开始查找错误。如果需要,你甚至可以轻松恢复更改。

  • 由于您发布的频率很高,您可以从每个人那里快速获得反馈。如果需要,您可以及时更改计划,而不是等上几个月才得到反馈,浪费您在这次发布上付出的所有努力。

这个想法看似完美,但我们该如何实施呢?首先,让我们关注流程中的手动部分:使用版本控制系统(VCS)开发功能。下图展示了一种非常常见的方法:

image 2023 11 04 13 50 38 075

正如我们已经提到的,VCS 允许开发人员在同一个代码库上工作,跟踪每个人所做的所有改动,并帮助解决冲突。VCS 通常允许你拥有不同的分支;也就是说,你可以偏离开发主线,继续开展工作,而不会扰乱主线。上图展示了如何使用分支来编写新功能,可以解释如下:

  • A: 某团队需要开始开发功能 A,他们从主分支创建了一个新分支,并在其中添加了对该功能的所有修改。

  • B:另一个团队也需要开始开发一个功能。他们从主分支创建了一个新分支,与之前的分支相同。此时,他们并不知道第一个团队在做什么,因为他们是在自己的分支上做的。

  • C:第二小组完成工作。没有其他人更改主分支,因此他们可以直接合并自己的更改。此时,CI 流程将启动发布流程。

  • D: 第一个团队完成了功能。为了将其合并到主版本,他们需要先将自己的分支与主版本的新变更进行重定向,并解决可能发生的任何冲突。分支越老,发生冲突的几率就越大,因此可以想象,较小和较快的功能是首选。

现在,让我们看看自动化流程是怎样的。下图显示了从合并到主版本到生产部署的所有步骤:

image 2023 11 04 13 53 35 299

在将代码合并到主分支之前,您一直处于开发环境中。CI 工具会监听项目主分支上的所有变更,并为每项变更触发一个作业。如有必要,该作业将负责构建项目,然后运行所有测试。如果出现任何错误或测试失败,它会立即通知所有人,触发该作业的团队应负责修复。此时,主分支被视为不稳定分支。

如果所有测试都通过了,CI 工具就会将你的代码部署到暂存中。暂存环境尽可能模拟生产环境,即具有相同的服务器配置、数据库结构等。一旦应用程序部署到这里,您就可以运行所有需要的测试,直到您有信心继续部署到生产环境。当您做出微小改动时,您不需要手动测试所有内容。相反,您可以测试您的更改和应用程序的主要用例。

我们说过,CI 的目标是使流程尽可能自动化。但是,我们仍然需要手动测试暂存中的应用程序,对吗?验收测试来帮忙!

编写单元测试固然很好,也是必须的,但它们只能以孤立的方式测试一小部分代码。即使你的整个单元测试套件都通过了,你也不能确定你的应用程序是否能正常运行,因为你可能没有正确地集成所有部分,因为你缺少功能,或者你构建的功能不是业务所需的。验收测试测试特定用例的整个流程。

如果您的应用程序是一个网站,验收测试很可能会启动浏览器并模拟用户操作,如点击和键入,以确定页面是否返回预期结果。是的,只需几行代码,您就能以自动化的方式执行以前需要手动执行的所有测试。

现在,想象一下您为应用程序的所有功能都编写了验收测试。一旦代码进入暂存阶段,CI 工具就能自动运行所有这些测试,并确保新代码不会破坏任何现有功能。您甚至可以根据需要使用不同的浏览器运行这些测试,以确保您的应用程序在所有浏览器中都能正常运行。如果测试失败,CI 工具会通知负责的团队,他们必须进行修复。如果所有测试都通过了,CI 工具就会自动将代码部署到生产环境中。

如果验收测试能测试业务真正关心的东西,那么我们为什么还需要编写单元测试呢?同时保留验收测试和单元测试有几个原因;事实上,单元测试的数量应该远远多于验收测试。

单元测试 vs 可接受测试

  • 单元测试检查的是一小段代码,因此比验收测试(针对浏览器测试整个流程)要快很多。这意味着您可以在几秒或几分钟内运行所有单元测试,但运行所有验收测试则需要更长的时间。

  • 编写绝对涵盖所有可能的用例组合的验收测试几乎是不可能的。为给定的方法或代码片段编写能覆盖大部分用例的单元测试则相当容易。您应该有大量的单元测试来测试尽可能多的边缘用例,但只需要一些验收测试来测试主要用例。

那么什么时候应该运行每种类型的测试呢?单元测试速度较快,因此应在部署的第一阶段执行。只有当我们知道所有测试都已通过后,我们才需要花时间部署到暂存阶段并运行验收测试。

TDD vs BDD

在第 7 章 "测试 Web 应用程序" 中,你了解到 TDD 或测试驱动开发是一种先编写单元测试后编写代码的做法,目的是编写可测试的、更简洁的代码,并确保测试套件始终保持最新。随着验收测试的出现,TDD 演变为 BDD 或行为驱动开发。

BDD 与 TDD 非常相似,都是先编写测试,然后编写使测试通过的代码。唯一不同的是,在 BDD 中,我们要编写指定代码所需行为的测试,这些行为可以转化为验收测试。尽管这始终取决于具体情况,但你还是应该编写测试应用程序特定部分的验收测试,而不是编写包含多个步骤的冗长用例。使用 BDD 和使用 TDD 一样,您都希望快速获得反馈,如果您编写了一个宽泛的测试,您就必须编写大量代码才能使其通过,而这并不是 BDD 想要实现的目标。

测试业务

验收测试和 BDD 的全部意义在于确保应用程序按预期运行,而不仅仅是代码。因此,验收测试不应由开发人员编写,而应由业务部门自己编写。当然,你不能指望经理和高管为了创建验收测试而去学习如何编写代码,但有很多工具可以让你把简单的英文说明或行为规范转化为验收测试的代码。当然,这些说明必须遵循一些模式。行为规范包括以下部分:

  • 标题,以非常清晰的方式简要描述行为规范所涵盖的用例。

  • 叙述,说明测试由谁执行、业务价值是什么以及预期结果是什么。叙述的格式通常如下:

    In order to <business value>
    As a <stakeholder>
    I want to <expected outcome>
  • 场景集,即我们要涵盖的每个特定用例的描述和步骤集。每个场景都有一个描述和一个说明列表,格式为 Given-When-Then;我们将在下一节详细讨论。常见的模式有:

    Scenario: <short description>
    Given <set up scenario>
    When <steps to take>
    Then <expected outcome>

在接下来的两节中,我们将发现两个 PHP 工具,您可以使用它们来理解行为场景并将其作为验收测试运行。