最近,我發表了一篇文章,介紹如何使用Testcontainers來模擬外部依賴,比如數據庫和緩存,以進行後端集成測試。該文章還解釋了運行集成測試的不同方法、環境搭建以及它們的優缺點。

在這篇文章中,我想展示另一種替代方案,以防你使用GitHub Actions作為你的CI平台(目前最流行的CI/CD解決方案)。這個替代方案叫做服務容器,我發現並不是很多開發者似乎知道它。

在這個實用的教程中,我將演示如何為帶有外部依賴(MongoDB和Redis)的集成測試創建GitHub Actions工作流,使用我們在之前的教程中創建的演示Go應用程序。我們還將回顧GitHub服務容器的優缺點。

前提條件

  • 對GitHub Actions工作流有基本了解。

  • 熟悉Docker容器。

  • Go 工具鏈的基本知識。

內容目錄

什麼是服務容器?

服務容器是 Docker 容器,提供一種簡單和可攜帶的方式來托管您的應用程序在工作流程中所需的依賴項,如數據庫(例如我們的例子中的 MongoDB)、網絡服務或緩存系統(例如我們的例子中的 Redis)。

本文專注於整合測試,但服務容器還有許多其他可能的應用。例如,您還可以使用它們來運行工作流程所需的支持工具,例如代碼分析工具、代碼檢查工具或安全掃描器。

為什麼不使用 Docker Compose?

聽起來和 Docker Compose 中的 服務 相似,對吧?那是因為它確實是。

但雖然您在 GitHub Actions 工作流程中技術上可以使用 Docker Compose 通過安裝 Docker Compose 並運行 docker-compose up 來達到,服務容器提供了一種更集成和流暢的方法,專門為 GitHub Actions 環境設計。

同時,儘管它們相似,但它們解決不同的問題並具有不同的一般目的:

  • 當您需要在本地機器或單個服務器上管理多容器應用程序時,Docker Compose 非常適用。它最適用於長期運行的環境。

  • 服務容器是短暫的,僅在工作流程運行期間存在,並且直接在您的 GitHub Actions 工作流程文件中定義。

請記住,目前(至少截至目前)服務容器的功能集比 Docker Compose 更有限,因此請準備好發現一些潛在的瓶頸。我們將在本文末尾介紹其中一些。

作業運行時間

您可以直接在運行機器上或在 Docker 容器中運行 GitHub 工作(通過指定 container 屬性)。第二個選擇通過使用您在 services 部分定義的標籤來簡化對服務的訪問。

要直接在運行機器上運行:

.github/workflows/test.yaml

jobs:
  integration-tests:
    runs-on: ubuntu-24.04

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017:27017

    steps:
      - run: |
          echo "addr 127.0.0.1:27017"

或者您可以在容器中運行它(在我們的案例中是 Chainguard Go Image):

jobs:
  integration-tests:
    runs-on: ubuntu-24.04
    container: cgr.dev/chainguard/go:latest

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017:27017
    steps:
      - run: |
          echo "addr mongo:27017"

您還可以省略主機端口,這樣容器端口將隨機分配給主機上的空閒端口。然後您可以使用變量訪問該端口。

省略主機端口的好處:

  • 避免端口衝突 – 例如,當您在同一主機上運行多個服務時。

  • 增強可攜性 – 您的配置變得不那麼依賴於特定的主機環境。

jobs:
  integration-tests:
    runs-on: ubuntu-24.04
    container: cgr.dev/chainguard/go:1.23

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017/tcp
    steps:
      - run: |
          echo "addr mongo:${{ job.services.mongo.ports['27017'] }}"

當然,每種方法都有其優缺點。

在容器中運行:

  • 優點:簡化的網絡訪問(使用標籤作為主機名),以及容器網絡內的自動端口暴露。您還可以獲得更好的隔離/安全性,因為工作在隔離的環境中運行。

  • 缺點:容器化的隱含開銷。

在運行者機器上運行:

  • 優點:潛在的開銷比在容器內運行任務少。

  • 缺點:需要手動端口映射以訪問服務容器(使用localhost:)。隔離/安全性也較低,因為任務直接在運行者機器上運行。如果出現問題,這可能影響其他任務或運行者本身。

就緒健康檢查

在運行連接到您配置的容器的集成測試之前,您通常需要確保服務已經準備好。您可以通過指定docker創建選項,例如health-cmd來做到這一點。

這非常重要 – 否則當您開始訪問服務時,這些服務可能無法準備好。

在 MongoDB 和 Redis 的情況下,將會是以下這些:

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017/27017
        options: >-
          --health-cmd "echo 'db.runCommand("ping").ok' | mongosh mongodb://localhost:27017/test --quiet"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10

      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10

在操作日誌中,您可以看到準備狀態:

私有容器註冊表

在我們的例子中,我們使用來自 Dockerhub 的公共映像,但也可以使用來自您私有註冊表的私有映像,例如亞馬遜彈性容器註冊表 (ECR)、谷歌藝術品註冊表等。

請確保將憑證存儲在 Secrets 中,然後在 credentials 部分引用它們。

services:
  private_service:
    image: ghcr.io/org/service_repo
    credentials:
      username: ${{ secrets.registry_username }}
      password: ${{ secrets.registry_token }}

在服務之間共享數據

您可以使用卷在服務或作業中的其他步驟之間共享數據。您可以指定命名的 Docker 卷、匿名的 Docker 卷或主機上的綁定掛載。但是,直接將源代碼掛載為容器卷是不可能的。您可以參考這個 開放討論 獲取更多背景信息。

要指定一個卷,您需要指定源和目標路徑: <source>:<destinationPath>

<source> 是卷的名稱或主機上的絕對路徑,<destinationPath> 是容器中的絕對路徑。

volumes:
  - /src/dir:/dst/dir

Docker(以及使用 Docker 的 GitHub Actions)中的卷提供了持久數據存儲和容器或作業步驟之間的共享,將數據與容器映像解耦。

項目設置

在深入完整源代碼之前,讓我們為執行與 GitHub 服務容器的集成測試設置項目。

  1. 創建一個新的 GitHub 倉庫。

  2. 使用 go mod init 初始化 Go 模塊

  3. 創建一個簡單的 Go 應用程序。

  4. integration_test.go 中添加集成測試

  5. 創建一個 .github/workflows 目錄。

  6. .github/workflows 目錄中創建一個名為 integration-tests.yaml 的文件。

Golang 集成測試

現在我們可以配置外部依賴,讓我們看看如何在 Go 中運行集成測試。我們將在工作流程文件的 步驟 部分進行。

我們將在使用 Chainguard Go image 的容器中運行測試。這意味著我們不需要安裝/設置 Go。如果您想直接在運行機器上運行測試,您需要使用 setup-go 操作。

您可以在這裡找到包含測試和工作流程的完整源代碼 here

.github/workflows/integration-tests.yaml

name: "integration-tests"

on:
  workflow_dispatch:
  push:
    branches:
      - main

jobs:
  integration-tests:
    runs-on: ubuntu-24.04
    container: cgr.dev/chainguard/go:latest

    env:
      MONGO_URI: mongodb://mongo:27017
      REDIS_URI: redis://redis:6379

    services:
      mongo:
        image: mongodb/mongodb-community-server:7.0-ubi8
        ports:
          - 27017:27017
        options: >-
          --health-cmd "echo 'db.runCommand("ping").ok' | mongosh mongodb://localhost:27017/test --quiet"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10

      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 10s
          --health-retries 10

    steps:
      - name: Check out repository code
        uses: actions/checkout@v4

      - name: Download dependencies
        run: go mod download

      - name: Run Integration Tests
        run: go test -tags=integration -timeout=120s -v ./...

總結這裡正在進行的操作:

  1. 我們在一個包含 Go 的容器中運行作業(container

  2. 我們啟動兩個服務:MongoDB 和 Redis(services

  3. 我們配置健康檢查,以確保在運行測試時我們的服務處於“健康”狀態(options

  4. 我們執行標準的代碼檢查

  5. 然後運行 Go 測試

一旦操作完成(本例中需要〜1分鐘),所有服務將被停止並處於孤立狀態,因此我們不需要擔心。

個人經驗和限制

我們在BINARLY一段時間內一直在使用服務容器運行後端集成測試,它們運行良好。但最初的工作流程創建花費了一些時間,我們遇到了以下瓶頸:

  • 無法在操作服務容器中覆蓋或運行自定義命令(就像在Docker Compose中使用command屬性那樣)。開啟拉取請求

    • 解決方法:我們必須找到一個不需要這樣做的解決方案。在我們的情況下,我們很幸運,可以使用環境變量做同樣的事情。
  • 無法將原始碼直接掛載為容器卷。 開放討論

    • 儘管這確實是一個很大的限制,但您可以在服務容器啟動後將程式碼從存儲庫複製到您的掛載目錄中。

結論

GitHub 服務容器是一個很好的選擇,可以通過在 GitHub 工作流程中直接配置來構建一個短暫的測試環境。由於配置與 Docker Compose 有些相似,因此很容易運行任何容器化應用程序並在流水線中與之通信。這確保 GitHub Runner 在完成後負責關閉所有事物。

如果您使用 Github Actions,這種方法非常適用,因為它是專為 GitHub Actions 環境設計的。

資源