改进 Phoenix 中的测试和持续集成
发布于 2021 年 1 月 15 日,作者 Aaron Renner
改进 Phoenix 中的测试和持续集成 持续集成 (CI) 是一项强大的工具。大型开源项目需要一套单元测试、一些集成测试以及自动运行这些测试的管道。然而,CI 也并非没有困难。构建失败、复杂的设置和缓慢的迭代周期可能会让人们厌恶等待他们的 PR 被构建。本指南将介绍我们如何处理 Phoenix 项目的测试和 CI,以及最近的更改如何使该过程更加顺畅。
Phoenix 的测试套件
Phoenix 有 4 个不同的测试套件,每个套件都有不同的用途和不同的依赖项。
测试套件 | 用途 |
---|---|
主测试<br>位置:/test<br>依赖项:<br> - Elixir | Phoenix 框架的核心测试套件。测试诸如端点、通道、路由器、控制器等内容。 |
安装程序测试<br>位置:/installer/test<br>依赖项:<br> - Elixir | 用于 mix phx.new 生成器的测试套件。这些测试确保代码生成器在正确的位置写入正确的代码。 |
集成测试<br>位置:/integration_test<br>依赖项:<br> - Elixir<br> - PostgresSQL<br> - MySQL<br> - MSSQL | 端到端测试 Phoenix 代码生成体验。这些测试使用 mix phx.new 创建一个新项目,运行一个或多个 mix phx.gen.* 命令,并确保没有编译警告、代码格式正确以及生成的测试套件通过。 |
JavaScript 测试 | 测试 Phoenix JavaScript 代码,用于套接字、通道和存在。 |
我们如何在本地测试
能够下载一个项目并在本地轻松运行其测试套件,是欢迎社区贡献的关键。Phoenix 使用 ExUnit,它与 Elixir 一起提供,因此运行主测试套件再简单不过了。
> mix test
....
Finished in 24.8 seconds
11 doctests, 737 tests, 0 failures
安装程序测试套件同样易于运行……只需在 /installer
文件夹中运行 mix test
即可。
然而,当我们开始更改代码生成器时,事情会变得更加复杂。虽然我们可以确保我们的生成器在正确的位置创建文件,但我们实际上不知道生成的代码是否有效,直到我们尝试运行它。为此,Phoenix 在 /integration_test 中有一个集成测试套件。
> tree
├── config
│ └── config.exs
├── docker-compose.yml
├── mix.exs
├── mix.lock
└── test
├── code_generation
│ ├── app_with_defaults_test.exs
│ ├── app_with_mssql_adapter_test.exs
│ ├── app_with_mysql_adapter_test.exs
│ ├── app_with_no_options_test.exs
│ └── umbrella_app_with_defaults_test.exs
├── support
│ └── code_generator_case.ex
└── test_helper.exs
要完全运行这些测试,我们需要访问三个独立的数据库:Postgres、MySQL 和 MSSQL。这通常很困难,但幸运的是,Phoenix 有一个 docker-compose 文件。
version: '3'
services:
postgres:
image: postgres
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: postgres
mysql:
image: mysql
ports:
- "3306:3306"
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
mssql:
image: mcr.microsoft.com/mssql/server:2019-latest
environment:
ACCEPT_EULA: Y
SA_PASSWORD: some!Password
ports:
- "1433:1433"
这使我们能够启动这些数据库并像这样运行集成测试。
> docker-compose up -d
Starting integration_test_postgres_1 ... done
Starting integration_test_mssql_1 ... done
Starting integration_test_mysql_1 ... done
> mix test --include database
Finished in 230.6 seconds
32 tests, 0 failures
Randomized with seed 896813
我们在 CI 中如何测试
Phoenix 项目使用 GitHub Actions (GHA) 来运行其每个测试套件。与大多数 CI 服务一样,GitHub 依赖于自己的特定配置文件来安装构建依赖项、启动外部服务以及执行各种测试套件。
由于 Phoenix 的 CI 和本地测试环境使用不同的工具,我们最终得到了重复的配置和相同测试的不同环境。重复的配置会导致事情不同步时出现问题。不同的环境意味着我们对我们的测试在 GitHub 上的行为与我们的本地机器上的行为是否相同没有完全的信心。当事情在本地有效但在 CI 中无效时,将测试提交推送到 Github 并添加调试语句以尝试找出问题原因是一个极其耗时的过程。
如果我可以在本地机器上运行 CI 构建呢?
大约在这个时候,José 与 Vlad 讨论了本地开发和 CI 之间的不一致性。Vlad 建议我们尝试使用 Earthly,这是一种他创建的用于指定构建的开源格式。
Vlad 的洞察力在于,如果我们要以可以在任何地方运行的格式定义整个构建过程、单元测试、集成测试、服务设置等,那么重现构建失败将变得容易。
遵循这种方法,我们的构建将看起来像这样
Earthfile
FROM hexpm/elixir:1.11-erlang-21.0-alpine-3.12.0
all:
BUILD +test
BUILD +integration-test
test:
WORKDIR /src/
COPY . .
RUN mix test
integration-test:
WORKDIR /src/integration_test
COPY . .
RUN mix deps.get
WITH DOCKER --compose docker-compose.yml
RUN mix test --include database
END
然后,我们可以通过调用 earthly 并指定一个目标,在本地或 GHA 中运行各种构建目标,例如 all、test 或 integration-test。
> earthly -P +all
现在,如果集成测试在 GHA 运行中失败,我们可以有信心通过运行相同的命令在本地重现它。整个构建都是容器化的,这使得重现事情变得更容易。这不仅对于重现构建失败非常有用,而且对于在不进行推送和等待 GHA 运行的情况下处理构建过程本身也很有用。Earthly 甚至允许我们在构建管道中进入 shell,以便四处查看并诊断问题(稍后会详细介绍)。Earthfile 语法建立在 docker 层之上,因此如果我们的 mix.lock
文件没有改变,它将使用缓存的层,而不是尝试再次下载我们的依赖项。在一个我们重新开始旅行的后疫情世界中,我们甚至可以在飞机上处理构建管道。
测试多个依赖项版本
Phoenix 的构建管道比一次运行每个测试套件更复杂。Phoenix 的每个版本都需要与最新的 Elixir 版本以及所有支持的版本一起使用。OTP 也是如此。GHA 对这种情况有很好的支持,它提供了一个名为“矩阵策略”的功能。你定义一个参数矩阵,它将使用这些参数中的每一个来执行你的作业。
matrix:
include:
- elixir: 1.9.4
otp: 20.3.8.26
- elixir: 1.10.4
otp: 21.3.8.17
- elixir: 1.10.4
otp: 23.0.3
矩阵策略并行运行所有这些作业,这意味着测试许多版本不会影响我们的构建运行时间。对于像 Phoenix 这样的库来说,这是一个关键功能。然而,它确实使本地可重复性问题变得更加困难。如果我们正在运行最新支持的 OTP 版本,并且 PR 对于一个支持的旧 OTP 版本失败,我们可以尝试切换版本,或者可能只是依靠 GHA 构建来测试我们的更改。实际上,有时我们会最终依赖于 GHA,这意味着我们现在有更长的反馈周期。当然,这并非世界末日,但同样,它为贡献过程增加了更多摩擦。
在本地重现矩阵测试
同样,使用 earthly 可以使这种情况更容易处理。我们可以为 Elixir 和 OTP 版本引入参数,然后使用它们以任何我们想要的版本运行测试。无需本地环境更改或其他工具。
Phoenix 框架最终得到的解决方案看起来像这样
setup:
ARG ELIXIR=1.11
ARG OTP=23.0.0
FROM hexpm/elixir:$ELIXIR-erlang-$OTP-alpine-3.12.0
...
integration-test:
FROM +setup
COPY --dir assets config installer lib integration_test priv test ./
WORKDIR /src/integration_test
RUN mix deps.get
WITH DOCKER --compose docker-compose.yml
RUN mix test --include database
END
这使得在本地测试任何版本组合变得非常容易
> earthly -P --build-arg ELIXIR=1.11.0 --build-arg OTP=23.1.1 +integration-test
+integration-test | Including tags: [:database]
...
+integration-test | Finished in 210.5 seconds
+integration-test | 32 tests, 0 failures
+integration-test | Randomized with seed 330691
output | --> exporting outputs
=========================== SUCCESS ===========================
Earthly 的解决方案非常低摩擦,这真的很酷。此外,Earthly 还有一个 –interactive 标志,当构建中的某个步骤返回非零状态时,它会将我们弹出到一个 shell 中。
采用 Earthly
综上所述,我很高兴地宣布 Phoenix 项目的 CI 管道现在由 Earthly 提供支持。
Adam Gordon Bell 在 10 月初提交了 PR,在我们进行评估过程时,与他合作非常愉快。 最终版本 比本文中的示例更复杂,并且一直在不断发展。虽然还处于早期阶段,但它对我的工作流程来说是一个巨大的胜利。它并不能取代我在执行 TDD 周期时在本地运行测试,但我已经养成了在将 PR 推送到 Github 之前在本地运行 Earthly 构建的习惯,以尽量减少我在 Github Actions 上发生的构建失败数量。
就我个人而言,我认为 Earthly 是下一代 CI 工具的开始,它将有助于缩小本地构建和 CI 构建之间的差距。如果有时间,我强烈建议你尝试一下,看看你的想法。