在測試時,TestContainers 它可以簡化我們產生 Container 的步驟,配置 Container 的方式也相當的簡單、明確;從同事得知 TestContainers,周末則來研究一下使用方式。
系統通常會依賴其他的遠端服務,可能長的像下圖,當本機需要開發時,需要花費時間配置它們,甚至讓開發成本變得較高(連到雲端)
於是我們用 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)
目前支援下圖語言
感覺很棒,接下來就來看看我的使用心得
開發環境
- Windiows 11
- Docker
- ASP.NET Core
- Rider 2023.2
Container Builder
從官網得知 TestContainer 的起手式看起來很簡單
- ContainerBuilder:配置 Container。
- ContainerBuilder.StartAsync:啟動Container
With 開頭是 Container 的配置,閱讀起來很清楚
再來一個 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 請參考
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,理論上應該沒有甚麼難度才是(希望),當年,同事也是因為在公司環境整合上有些問題(權限)導致測試時好時壞,最後棄用它,接下來應該會再闖關試試。
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET