【读书笔记】《Go With The Domain》13. 在 CI/CD 管道中运行集成测试
📔【读书笔记】《Go With The Domain》13. 在 CI/CD 管道中运行集成测试
2023-10-4
| 2024-3-24
0  |  0 分钟
type
status
date
slug
summary
tags
category
icon
password
Sub-item
Last edited time
Mar 24, 2024 03:12 AM
Parent item
领域
这篇文章是测试架构的直接后续内容,我在其中为我们的示例项目引入了新类型的测试。
Wild Workouts 使用 Google Cloud Build 作为 CI/CD 平台。它以持续部署的方式进行配置,这意味着一旦管道通过,更改就会立即投入生产。如果你考虑一下我们目前的设置,你会发现它既勇敢又天真。我们没有在那里运行可以避免明显错误的测试(无论如何,不太明显的错误很少能被测试发现)。
在本章中,我将展示如何使用 docker-compose 在 Google Cloud Build 上运行集成测试、组件测试和端到端测试。

当前配置

让我们看一下当前的cloudbuild.yaml 文件。虽然非常简单,但由于我们在单个存储库中保留 3 个微服务,因此大多数步骤都会运行多次。我专注于后端部分,所以我现在将跳过与前端部署相关的所有配置。
注意 waitFor 键。它使一个步骤需要等待其他指定的步骤完成。有些作业可以通过这种方式并行运行。这是更易读的版本:
notion image
我们对每项服务都有类似的工作流程:lint(静态分析)、构建 Docker 镜像并将其部署为一个或两个 Cloud Run 服务。
由于我们的测试套件已准备就绪并可在本地运行,因此我们需要弄清楚如何将其插入管道中。

Docker Compose

我们已经有了一个 docker-compose 定义,我想保持这种方式。我们将使用它来:
  • 本地运行应用程序
  • 本地运行测试
  • 在CI 中运行测试
这三个目标有不同的需求。例如,当本地运行应用程序时,我们希望进行代码热重载。但这在 CI 中毫无意义。另一方面,我们无法在 CI 中公开本地主机上的端口,这是在本地环境中访问应用程序的最简单方法。
幸运的是,docker-compose 足够灵活,可以支持所有这些用例。我们将使用一个基本的 docker-compose.yml文件和一个附加的 docker-compose.ci.yml 文件,其中仅覆盖 CI。您可以通过使用 -f 标志传递两个文件来运行它(请注意,每个文件都有一个标志)。文件中的密钥将按提供的顺序合并。
💡
注意 通常,docker-compose 会在当前目录或父目录中查找 docker-compose.yml 文件。使用-f标志禁用此行为,因此仅解析指定的文件。
要在 Cloud Build 上运行它,我们可以使用 docker/compose 镜像。
由于我们用正确的步骤名称填充了 waitFor,因此我们可以确保存在正确的镜像。这是我们刚刚添加的内容:
notion image
docker-compose.ci.yml 的第一个改造是使每个服务通过标签使用 docker 镜像,而不是从 docker/app/Dockerfile 构建镜像。这确保我们的测试检查我们要部署的相同镜像。
请注意图像键中的 ${PROJECT_ID} 变量,表示项目产品,因此我们不能将其硬编码到存储库中。 Cloud Build 在每个步骤中都提供了此变量,因此我们只需将其传递给 docker-compose up 命令(请参阅上面的定义)。

网络

如今,许多 CI 系统都使用 Docker,通常在具有所选镜像的容器内运行每个步骤。在 CI 中使用 docker-compose 有点棘手,因为它通常意味着从 Docker 容器内运行 Docker 容器。
在 Google Cloud Build 上,所有容器都位于 cloudbuild 网络内。只需将此网络添加为 docker-compose.ci.yml 的默认网络就足以让 CI 步骤连接到 docker-compose 服务。这是我们的第二个改造:

环境变量

使用环境变量作为配置乍一看似乎很简单,但考虑到我们需要处理多少场景,它很快就会变得复杂。让我们尝试列出所有这些:
  • 在本地运行应用程序
  • 在本地运行组件测试
  • 在 CI 中运行组件测试
  • 在本地运行端到端测试
  • 在 CI 中运行端到端测试
我没有在生产环境中运行该应用程序,因为它不使用 docker-compose。
为什么组件测试和端到端测试是不同的场景?前者按需启动服务,后者与 docker-compose 中已运行的服务进行通信。这意味着两种类型将使用不同的端点来访问服务。
💡
注意 有关组件和端到端测试的更多详细信息,请参阅测试架构。 TL;DR:我们重点关注组件测试,其中不包括外部服务。端到端测试只是为了确认合同没有在非常高的水平上被破坏,并且仅针对最关键的路径。这是服务解耦的关键。
我们已经保留了一个包含大多数变量的基本 .env 文件。它被传递给 docker-compose 定义中的每个服务。
此外,当 docker-compose 在工作目录中找到该文件时,它会自动加载该文件。因此,我们也可以使用 yaml 定义中的变量。
我们还需要在运行测试时加载这些变量。在 bash 中这很容易做到:
但是,.env 文件中的变量没有导出前缀,因此它们不会进一步传递到 shell 中运行的应用程序。我们不能使用前缀,因为它与 docker-compose 期望的语法不兼容。
此外,我们无法将单个文件用于所有场景。我们需要覆盖一些变量,就像我们对 docker-compose 定义所做的那样。我的想法是为每个场景保留一个附加文件。它将与基本 .env 文件一起加载。
让我们看看所有场景之间有什么区别。为了清楚起见,我只包含了 users-http,但这个想法将适用于所有服务。
notion image
docker-compose 运行的服务使用端口 3000+,组件测试在端口 5000+ 上启动服务。这样,两个实例就可以同时运行。
我创建了一个 bash 脚本来读取变量并运行测试。请不要尝试直接在 Makefile 中定义如此复杂的场景。Make 在管理环境变量方面很糟糕。
创建专用脚本的另一个原因是我们将 3 个服务保留在一个存储库中,并将端到端测试保留在一个单独的目录中。如果我需要多次运行相同的命令,我更喜欢调用带有两个变量的脚本,而不是使用长长的标志和参数。第三个原因是它们可以用 shellcheck 进行检查。
该脚本在给定目录中运行go test,并使用从 .env 和指定文件加载的环境变量。env / xargs 技巧将所有变量传递给以下命令。请注意我们如何使用 grep 从文件中删除注释。
💡
测试缓存 go test 会缓存成功的结果,只要不修改相关文件即可。在使用 Docker 的测试中,您可能会更改基础设施级别的某些内容,例如 docker-compose 定义或某些环境变量。 go test 不会检测到这一点,并且您可能会将缓存的测试误认为是成功的测试。 对此很容易感到困惑,而且由于我们的测试无论如何都很快,因此我们可以禁用缓存-count=1 标志是一种惯用的(尽管不明显)方法。

运行测试

在所有服务的测试通过后,开始运行端到端测试。它应该类似于您通常运行它们的方式。请记住,端到端测试应该起到双重检查的作用,并且每个服务自己的测试应该具有最大的覆盖范围。
由于我们的端到端测试范围很小,因此我们可以在部署服务之前运行它们。如果它们运行很长时间,这可能会阻止我们的部署。在这种情况下,更好的想法是依赖每个服务的组件测试同时并行运行端到端套件。
我们添加的最后一件事是在所有测试通过后运行 docker-compose。这只是一次清理。
我们管道的第二部分现在看起来像这样:
notion image
以下是本地运行测试的样子(我在上一章中介绍了此Make Target)。它与CI完全相同,只有不同的.env文件

分离测试

查看上一章中的表格,根据是否使用 Docker,我们可以将测试分为两组。
notion image
单元测试是唯一不使用 Docker 数据库的类别,而集成、组件和端到端测试则使用 Docker 数据库。
尽管我们使所有测试都变得快速且稳定,但设置 Docker 基础设施会增加一些开销。单独运行所有单元测试很有帮助,可以首先防止错误。
我们可以使用构建标签来分隔不使用 Docker 的测试。您可以在文件的第一行定义构建标签。
我们现在可以与所有测试分开运行单元测试。例如,下面的命令将仅运行需要 Docker 服务的测试:
最后,我决定不引入这种分离。我们使测试足够稳定和快速,因此一次运行所有测试并不是问题。然而,我们的项目非常小,测试套件只涵盖了关键路径。当它增长时,在组件测试中引入构建标签可能是个好主意

题外话:一个关于 CI 调试的小故事

当我介绍本章的更改时,Cloud Build 上的初始测试运行一直失败。根据日志,测试无法从 docker-compose 访问服务。
因此,我开始调试并添加了一个简单的 bash 脚本,该脚本将通过 telnet 连接到服务。
令我惊讶的是,连接到 mysql:3306 工作正常,但 firestore:8787 没有,所有 Wild Workouts 服务也是如此。我认为这是因为 docker-compose 需要很长时间才能启动,但是多次重试都没有帮助。最后,我决定尝试一些疯狂的事情,我从 docker-compose 中的一个容器设置了一个反向 SSH 隧道。这允许我在构建仍在运行时在其中一个容器内进行 SSH。然后我尝试使用 telnet 和curl,它们对所有服务都能正常工作。
最后,我在我使用的 bash 脚本中发现了一个错误
变量定义中的拼写错误导致 telnet 命令运行变成了:telnet $host $host。那么为什么它对 MySQL 有效呢?事实证明,telnet 可以识别/etc/services 中定义的端口。因此 telnet mysql mysql 被转换为 telnet mysql 3306 并且工作正常,但对于任何其他服务都失败了。
为什么测试失败了?好吧,事实证明这是一个完全不同的原因。
最初,我们是这样连接MySQL的:
我查看了环境变量,所有这些都正确填写。添加一些 fmt.Println() 调试后,我发现配置的 Addr 部分被 MySQL 客户端完全忽略,因为我们没有指定 Net 字段。为什么它适用于本地测试?因为MySQL暴露在localhost上,这是默认地址。
另一个测试无法连接到 Wild Workouts 服务之一,结果是因为我在 .env 文件中使用了不正确的端口。
我为什么要分享这个?我认为这是一个很好的例子,展示了 CI 系统的工作方式。当多项事情都可能失败时,很容易得出错误的结论并在错误的方向上深入挖掘。
如有疑问,我喜欢使用基本工具来调查 Linux 问题,例如 strace、curl 或 telnet。这也是我使用反向 SSH 隧道的原因,我很高兴我这么做了,因为这似乎是调试 CI 内部问题的好方法。我觉得有时间我会再次使用它。

总结

我们设法保留一个 docker-compose 定义,用于在本地和管道中运行测试。从 git Push 到生产的整个 Cloud Build 运行需要 4 分钟。我们使用了一些巧妙的技巧,但这只是处理 CI 时的常规内容。有时你无法避免添加一些 bash 魔法来使事情正常进行。与领域代码相比,CI 设置中的黑客攻击不会对您造成太大伤害,只要其中只有少数。只要确保它很容易理解正在发生的事情,这样你就不是唯一拥有所有知识的人。
架构设计
  • 读书笔记
  • 技术架构
  • 【读书笔记】《Go With The Domain》2. gRPC通信【读书笔记】《Go With The Domain》5. DDD Lite
    目录