这是较早的Integration Testing MySQL Store的延续。在本教程中,我将扩展示例以使用testcontainers-dotnet旋转数据库容器并在执行集成测试之前应用迁移。
在此示例之前,运行集成测试的先决条件是数据库服务器在机器或容器中运行,并且应用了迁移。此步骤删除了手动步骤。
设置
让我们首先添加Nuget软件包
dotnet add package TestContainers.Container.Database.MySql --version 1.5.4
我们需要在运行集成测试之前启动2个容器。
- 数据库容器 - 托管数据库服务器
- 迁移容器 - 应用数据库迁移的容器
MigrationsContainer
我们将首先添加一个新的MigrationsContainer
从GenericContainer
继承。我们将添加一种使用db
文件夹的Dockerfile
创建图像的助手方法。
我们还将添加一个助手方法GetExitCodeAsync
,这将使用docker客户端等待迁移容器退出,我们需要此功能,以便我们仅在应用迁移后运行集成测试。
MigrationsContainer
看起来像
using Docker.DotNet;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TestContainers.Container.Abstractions;
using TestContainers.Container.Abstractions.Hosting;
using TestContainers.Container.Abstractions.Images;
namespace Movies.Api.Tests.Integration;
public class MigrationsContainer : GenericContainer
{
[ActivatorUtilitiesConstructor]
public MigrationsContainer(IDockerClient dockerClient, ILoggerFactory loggerFactory)
: base(CreateDefaultImage(), dockerClient, loggerFactory)
{
this.DockerClient = dockerClient;
}
internal IDockerClient DockerClient { get; }
public async Task<long> GetExitCodeAsync()
{
var containerWaitResponse = await this.DockerClient.Containers.WaitContainerAsync(this.ContainerId);
return containerWaitResponse.StatusCode;
}
private static IImage CreateDefaultImage()
{
return new ImageBuilder<DockerfileImage>()
.ConfigureImage((context, image) =>
{
image.DockerfilePath = "Dockerfile";
image.DeleteOnExit = true;
image.BasePath = "../../../../db";
})
.Build();
}
}
数据库固定
对于DatabaseFixture
,我们将添加以下字段
private readonly bool useServiceDatabase;
private readonly MySqlContainer? databaseContainer;
private MigrationsContainer? migrationsContainer;
useServiceDatabase
如果我们只想使用在主机上运行的数据库服务器或在容器中运行并调试测试,这将减少启动时间,同时调试和修复测试或运行红色 - 绿色 - 依赖器循环。
其他2个变量是保存对容器的引用,测试完成后,我们将使用这些变量进行清理。
我们将通过设置docker image使用ConfigureDockerImageName
并使用ConfigureDatabaseConfiguration
扩展方法配置用户名,密码和数据库名称来设置构造函数中的databasecontainer。构造函数看起来像
public DatabaseFixture()
{
this.useServiceDatabase = Debugger.IsAttached;
if (!this.useServiceDatabase)
{
Environment.SetEnvironmentVariable("REAPER_DISABLED", true.ToString());
this.databaseContainer = new ContainerBuilder<MySqlContainer>()
.ConfigureDockerImageName("mysql:5.7")
.ConfigureDatabaseConfiguration("root", "Password123", "defaultdb")
.Build();
}
}
初始酶
开始,如果我们在调试模式下运行,我们只会将连接字符串设置为指向已经在主机上或容器上运行的数据库服务器。
有趣的是,如果测试未在调试模式下运行,则开始。我们首先启动数据库容器。之后,我们将配置和构建迁移容器。这需要在此处完成,因为我们需要将连接字符串传递给数据库容器到迁移容器。
构建图像后,我们将启动迁移容器并等待其退出,如果出口代码为0,则表明迁移已成功应用。在这一点
InitializeAsync
方法的完整代码如下
public async Task InitializeAsync()
{
if (this.useServiceDatabase)
{
this.ConnectionString = "server=localhost;database=Movies;uid=root;password=Password123;SslMode=None;";
return;
}
await this.databaseContainer!.StartAsync();
this.migrationsContainer = new ContainerBuilder<MigrationsContainer>()
.ConfigureContainer((context, container) =>
{
var connectionString = $"server=localhost;Port={this.databaseContainer.GetMappedPort(MySqlContainer.DefaultPort)};database=defaultdb;uid=root;password=Password123;SslMode=None;";
container.Command = new List<string>
{
connectionString,
};
})
.ConfigureNetwork((hostContext, builderContext) =>
{
return new NetworkBuilder<UserDefinedNetwork>()
.ConfigureNetwork((context, network) => { network.NetworkName = "host"; })
.Build();
})
.Build();
await this.migrationsContainer.StartAsync();
var exitCode = await this.migrationsContainer.GetExitCodeAsync();
if (exitCode > 0)
{
throw new Exception("Database migration failed");
}
this.ConnectionString = $"server={this.databaseContainer.GetDockerHostIpAddress()};Port={this.databaseContainer.GetMappedPort(MySqlContainer.DefaultPort)};database=Movies;uid=root;password=Password123;SslMode=None;";
}
disposeasync
DisposeAsync
很简单,如果我们没有在调试模式下运行,我们会仔细检查数据库和迁移逆转录是否不是null,我们使用StopAsync
停止那些。
看起来如下如下
public async Task DisposeAsync()
{
if (this.useServiceDatabase)
{
return;
}
if (this.migrationsContainer != null)
{
await this.migrationsContainer.StopAsync();
}
if (this.databaseContainer != null)
{
await this.databaseContainer.StopAsync();
}
}
databaseFixture.cs
DatabaseFixture.cs
的完整来源如下
using System.Diagnostics;
using TestContainers.Container.Abstractions.Hosting;
using TestContainers.Container.Database.MySql;
using TestContainers.Container.Database.Hosting;
using TestContainers.Container.Abstractions.Networks;
namespace Movies.Api.Tests.Integration;
public class DatabaseFixture : IAsyncLifetime
{
public string ConnectionString { get; private set; } = default!;
private readonly bool useServiceDatabase;
private readonly MySqlContainer? databaseContainer;
private MigrationsContainer? migrationsContainer;
public DatabaseFixture()
{
this.useServiceDatabase = Debugger.IsAttached;
if (!this.useServiceDatabase)
{
Environment.SetEnvironmentVariable("REAPER_DISABLED", true.ToString());
this.databaseContainer = new ContainerBuilder<MySqlContainer>()
.ConfigureDockerImageName("mysql:5.7")
.ConfigureDatabaseConfiguration("root", "Password123", "defaultdb")
.Build();
}
}
public async Task InitializeAsync()
{
if (this.useServiceDatabase)
{
this.ConnectionString = "server=localhost;database=Movies;uid=root;password=Password123;SslMode=None;";
return;
}
await this.databaseContainer!.StartAsync();
this.migrationsContainer = new ContainerBuilder<MigrationsContainer>()
.ConfigureContainer((context, container) =>
{
var connectionString = $"server=localhost;Port={this.databaseContainer.GetMappedPort(MySqlContainer.DefaultPort)};database=defaultdb;uid=root;password=Password123;SslMode=None;";
container.Command = new List<string>
{
connectionString,
};
})
.ConfigureNetwork((hostContext, builderContext) =>
{
return new NetworkBuilder<UserDefinedNetwork>()
.ConfigureNetwork((context, network) => { network.NetworkName = "host"; })
.Build();
})
.Build();
await this.migrationsContainer.StartAsync();
var exitCode = await this.migrationsContainer.GetExitCodeAsync();
if (exitCode > 0)
{
throw new Exception("Database migration failed");
}
this.ConnectionString = $"server={this.databaseContainer.GetDockerHostIpAddress()};Port={this.databaseContainer.GetMappedPort(MySqlContainer.DefaultPort)};database=Movies;uid=root;password=Password123;SslMode=None;";
}
public async Task DisposeAsync()
{
if (this.useServiceDatabase)
{
return;
}
if (this.migrationsContainer != null)
{
await this.migrationsContainer.StopAsync();
}
if (this.databaseContainer != null)
{
await this.databaseContainer.StopAsync();
}
}
}
测试
现在Run Test
应自动执行以下
- 旋转数据库容器
- 为迁移创建图像
- 旋转迁移容器以执行迁移
- 执行集成测试
- 停止容器
这是本文的全部,这是在运行集成测试之前自动串起数据库和迁移。
CI的集成测试
我还添加了GitHub Actions工作流程以在将更改推向main
分支时作为CI的一部分运行这些集成测试。
我们将使用Building and testing .NET指南中定义的标准步骤。运行数据库服务器和迁移将通过DatabaseFixture
在Tests.Integration
项目中照顾。
这是工作流的完整列表。
name: Integration Test MySQL (testcontainers-dotnet)
on:
push:
branches: [ "main" ]
paths:
- 'integration-test-mysql-with-testcontainers-dotnet/**'
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: integration-test-mysql-with-testcontainers-dotnet
steps:
- uses: actions/checkout@v3
- name: Setup .NET Core SDK
uses: actions/setup-dotnet@v3
with:
dotnet-version: 7.0.x
- name: Install dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Run integration tests
run: dotnet test --configuration Release --no-restore --no-build --verbosity normal
来源
演示应用程序的源代码托管在blog-code-samples存储库中的GitHub上。
Integration Test MySQL (testcontainers-dotnet)
工作流的来源在integration-test-mysql-testcontainers-dotnet.yml中。
参考
没有特定顺序