提升 Phoenix 中的測試和持續整合

Aaron Renner 於 2021 年 1 月 15 日星期五發布


提升 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 測試 測試 Sockets、Channels 和 Presence 的 phoenix JavaScript 程式碼。

我們的本地測試方式

能夠下載專案並輕鬆地在本地執行其測試組,是歡迎社群貢獻的關鍵。Phoenix 使用隨 Elixir 一起提供的 ExUnit,因此執行主測試組非常簡單

> 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

我們在持續整合中測試的方式

Phoenix 專案使用 GitHub Actions (GHA) 來執行各組測試套件。與大多數的持續整合服務一樣,GitHub 仰賴其專屬組態檔來安裝建置相依項,啟動外部服務,並執行各種測試套件。

由於 Phoenix 的持續整合和在地測試環境使用不同的工具,導致同組測試出現重複組態和不同環境的狀況。重複組態在不同步時會造成問題。不同的環境表示,我們無法完全確定測試在 GitHub 上執行時,其行為是否會與我們的在地機器相同。且當程式在在地端執行正常,但在持續整合中異常時,必須花費非常久的時間將測試提交推送到 GitHub,並加入除錯敘述,才能找出問題原因。

如果我可以在我的在地機器上執行持續整合建置呢?

這段時間,José 正在和 Vlad 討論在地開發和持續整合之間的差異。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 中執行各種建置目標,例如全部、測試或整合測試。

> earthly -P +all

現在,如果在 GHA 執行中整合測試失敗,我們有信心能透過執行相同指令,在在地端重現問題。整個建置都已容器化,讓重現問題的過程容易許多。這種方式不僅有助於重現建置失敗,也能讓建置程序本身更容易進行作業,無須推送並等到 GHA 執行才能作業。Earthly 甚至讓我們得以中斷建置管線中的殼層,探究並診斷問題(稍後會詳細說明)。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 還有一個「互動式」標記,當建置步驟傳回非零狀態時,會彈出一個殼牌。

採用 Earthly

綜上所述,我很興奮地宣布 Phoenix 專案目前的 CI 作業管道是由 Earthly 進行推動。

亞當·高登·貝爾 於 10 月初提交了 PR,在評估過程中,他樂意協助解答我們的問題。 最後的版本 比本文中的範例更為複雜,還會持續演進。目前仍屬初期階段,但對我的工作流程而言,這是一個莫大的勝利。這不會取代我在進行 TDD 循環時的本地測試,但我已經養成一個習慣,在將 PR 推送出去之前,先在本地執行 Earthly 建置,以便減少我在 GitHub Actions 中建置失敗的次數。

我個人認為 Earthly 是新一代 CI 工具的開端,有助於縮小本地建置和 CI 建置之間的差距。如果你時間充裕,我強烈建議你親身體驗並分享你的想法。