通過 TestContainers 產生測試所依賴的環境

在測試時,TestContainers 它可以簡化我們產生 Container 的步驟,配置 Container 的方式也相當的簡單、明確;從同事得知 TestContainers,周末則來研究一下使用方式。

Test workflow

系統通常會依賴其他的遠端服務,可能長的像下圖,當本機需要開發時,需要花費時間配置它們,甚至讓開發成本變得較高(連到雲端)

雲原生微服務

於是我們用 Docker + Docker Compose 解決配置以及成本的問題,以下是一個 Docker Dompose 的配置,但是在 CI/CD 時需要額外的 Pipeline 啟動/結束它


version: "3.8"

services:
  redis:
    image: redis
    ports:
      - 6379:6379

  # 在登入頁面 
  # host:redis
  # port:6379
  redis-admin:
    image: marian/rebrow
    ports:
      - 5001:5001
    depends_on:
      - redis

 

Test Container 解決了甚麼問題

雖然用了 Docker/Docker-Compose,但啟動 Container 跟測試程式的生命週期仍然是分開的,TestContainer 可以讓我們在測試程式碼,啟動/停止 Container,這也意味著 CI/CD 的 Pipeline 可以不需要控制 Container start/stop

下圖出自:Getting Started (testcontainers.com)

Test workflow

目前支援下圖語言

 

感覺很棒,接下來就來看看我的使用心得

開發環境

  • Windiows 11
  • Docker
  • ASP.NET Core
  • Rider 2023.2

 

Container Builder

從官網得知 TestContainer 的起手式看起來很簡單

  • ContainerBuilder:配置 Container。
  • ContainerBuilder.StartAsync:啟動Container

Testcontainers for .NET

 

With 開頭是 Container 的配置,閱讀起來很清楚

Creating a container - Testcontainers for .NET

 

再來一個 PostgreSql Container 的例子,熟悉下它

[TestMethod]
public async Task GenericContainer()
{
    var waitStrategy = Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready");
    var postgreSqlContainer = new ContainerBuilder()
        .WithImage("postgres:12-alpine")
        .WithName("postgres.12")
        .WithPortBinding(5432)
        .WithWaitStrategy(waitStrategy)
        .WithEnvironment("POSTGRES_USER", "postgres")
        .WithEnvironment("POSTGRES_PASSWORD", "postgres")
        .Build();
    await postgreSqlContainer.StartAsync()
        .ConfigureAwait(false);

    var connectionString = "Host=localhost;Port=5432;Username=postgres;Password=postgres;Database=postgres";
    await using DbConnection connection = new NpgsqlConnection(connectionString);
    await using DbCommand command = new NpgsqlCommand();
    await connection.OpenAsync();
    command.Connection = connection;
    command.CommandText = "SELECT 1";
}

 

參數說明如下:

  • .WithWaitStrategy:用於檢測容器是否已準備好進行測試
  • .WithPortBinding:設定 Container 的 Port:5432,對應到宿主的 Port:5432
  • .WithEnvironment:設定 Container 的環境變數

Module Name Container

為了更容易配置 Container,基於 ContainerBuilder 的擴展,針對各 Container 的 Environment 配置,定義出相關的參數,叫做 Models Container

以 PostgreSQL 為例子

dotnet add package Testcontainers.PostgreSql --version 3.5.0

 

範例如下

[TestMethod]
public async Task ModuleContainer()
{
    var waitStrategy = Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready");
    var postgreSqlContainer = new PostgreSqlBuilder()
        .WithImage("postgres:12-alpine")
        .WithName("postgres.12")
        .WithPortBinding(5432, assignRandomHostPort: true)
        .WithWaitStrategy(waitStrategy)
        .WithUsername("postgres")
        .WithPassword("postgres")
        .Build();
    await postgreSqlContainer.StartAsync()
        .ConfigureAwait(false);

    var connectionString = postgreSqlContainer.GetConnectionString();
    await using DbConnection connection = new NpgsqlConnection(connectionString);
    await using DbCommand command = new NpgsqlCommand();
    await connection.OpenAsync();
    command.Connection = connection;
    command.CommandText = "SELECT 1";
}

原本要用 WithEnvironment 配置帳號密碼,現在可以改用 .WithUsername、.WithPassword,更棒的是,可以取得動態產生出來的連線字串,下圖可以看到動態產出的 port,可以透過 postgreSqlContainer.GetConnectionString() 方法取得

 

更多的 Modules 請參考

Modules - Testcontainers for .NET

 

Create Docker Image

除了,從 docker registry 安裝測試所需要的環境, TestContainers 也支援從 Dockerfile 建立 Container

建立一個 ASP.NET Core 7 WebAPI 的專案,隨便寫個端點回傳 "OK~"

[HttpGet(Name = "GetDemo")]
public ActionResult Get()
{
    return this.Ok("OK~");
}

 

加入 Dockerfile

 

由 Rider 產生出來的 Dockerfile 內容如下

FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["Lab.TestContainers.WebApi/Lab.TestContainers.WebApi.csproj", "Lab.TestContainers.WebApi/"]
RUN dotnet restore "Lab.TestContainers.WebApi/Lab.TestContainers.WebApi.csproj"
COPY . .
WORKDIR "/src/Lab.TestContainers.WebApi"
RUN dotnet build "Lab.TestContainers.WebApi.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "Lab.TestContainers.WebApi.csproj" -c Release -o /app/publish /p:UseAppHost=false

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Lab.TestContainers.WebApi.dll"]

 

接下來,通過 ImageFromDockerfileBuilder 產生 Docker Image,這裡要注意 Dockerfile 的位置是不是正確的

var solutionDirectory = CommonDirectoryPath.GetSolutionDirectory();
var dockerFilePath = "Lab.TestContainers.WebApi/Dockerfile";
var imageBuilder = new ImageFromDockerfileBuilder()
    .WithDockerfileDirectory(solutionDirectory, string.Empty)
    .WithDockerfile(dockerFilePath)
    .WithName("my.aspnet.core.7")
    .Build();

await imageBuilder.CreateAsync()
    .ConfigureAwait(false);

 

啟動 Container

var container = new ContainerBuilder()
        .WithImage("my.aspnet.core.7")
        .WithName("my.aspnet.core.7")
        .WithPortBinding(80)
        .Build()
    ;
await container.StartAsync()
    .ConfigureAwait(false);
;

 

打端點,回傳 "OK~"

var scheme = "http";
var host = "localhost";
var port = container.GetMappedPublicPort(80);
var url = "demo";

var httpClient = new HttpClient();
var requestUri = new UriBuilder(scheme, host, port, url).Uri;
var actual = await httpClient.GetStringAsync(requestUri);
Assert.AreEqual("OK~", actual);

 

的確也在 Containers Viewer 內看到我剛剛建立的 my.aspnet.core.7 的 Container

心得

 TestContainers 我很快速的玩過一遍,在測試專案裡啟動 Container,的確省掉了我手動啟動 Containers  的步驟,讓測試流程感覺更順暢, 還沒玩到整合 CI/CD,理論上應該沒有甚麼難度才是(希望),當年,同事也是因為在公司環境整合上有些問題(權限)導致測試時好時壞,最後棄用它,接下來應該會再闖關試試。

範例位置

sample.dotblog/Test/Lab.Test.Container at ecd9dc07f17680135db548528eec8bd11bca700d · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo