A simple builder for creating your integration test infrastructure.
Explore the docs »
Samples
·
Request Feature
This project was created with the intention of making easier/generic the setup a infrastructure using containers for integration tests.
The initial version only has support for docker desktop, but we have no intinction on stopping there, the main goal is to be agnostic how the definition of the infrastructure is done either for local development and CI, without having to force one tool (docker desktop, minikube...) to everyone else.
- Create a simple setup builder for defining the infrastructure.
- Support local and remote images.
- Provide an overridable method for evaluation of the containerized readiness,
- Provide an overridable method for preparation of the service containerized before being available to the infrastructure,
- Provide an overridable method for build of the local images.
- Automatic clean up of the infrastructure.
- Create documentation.
- Remove the need of the extension UsingDocker.
- Move user configs and tools settings to a config file.
- Use an environment variable to evaluate witch tool to use (for CI only).
- Provide out of the box support for generating valid and expired certificates.
- Add base functionality that allow an "ease" way to replicate infrastructure issues (service restarts, addition and removal of services nodes, network availability issues).
Explore the examples
Add the package to your project
- Package Manager
PM> Install-Package ToDoLater.Containerized.Infrastructure.Docker
- PackageReference
<PackageReference Include="ToDoLater.Containerized.Infrastructure.Docker"/>
- Create a class which inherits from DockerContainerizedService.
- Override the IsReadyAsync to define how the check for readiness should be done.
- If you need to prepared the service (add a user, create a database..) override the PrepareAsync.
public class SqlContainerizedFixture : DockerContainerizedService
{
...
public override async Task<bool> IsReadyAsync(CancellationToken cancellationToken)
{
try
{
var port = this.settings.PortMappings.First().Key;
using (var conn = new SqlConnection($"Server=localhost,{port};User=sa;Password=!MyMagicPasswOrd;Timeout=5;TrustServerCertificate=true"))
{
await conn.OpenAsync();
return true;
}
}
catch (Exception)
{
// while we are not able to establish a connection and while the cancellationToken hasn't expired the system will continue to try after a short delay.
return false;
}
}
public override async Task<bool> PrepareAsync(CancellationToken cancellationToken)
{
var port = this.settings.PortMappings.First().Key;
using (var conn = new SqlConnection($"Server=localhost,{port};User=sa;Password=!MyMagicPasswOrd;Timeout=5;TrustServerCertificate=true"))
{
await conn.OpenAsync();
foreach (var query in Queries)
{
using (var command = new SqlCommand(query, conn))
{
command.ExecuteNonQuery();
}
}
return true;
}
}
private static List<string> Queries = new List<string>
{
@"IF DB_ID('WeatherForecastdb') IS NULL
BEGIN CREATE DATABASE WeatherForecastdb;
END",
...
};
}
- Create a class with the test infrastructure setup.
public class InfrastructureWithExistingRecords : IAsyncLifetime
{
protected IContainerizedInfrastructure containerizedInfrastructure;
...
public InfrastructureWithExistingRecords()
{
this.containerizedInfrastructure = ContainerizedInfrastructureBuilder.UsingDocker
.With<SqlContainerizedFixture>(() => new DockerServiceSettings(
name : "db1",
image: "mcr.microsoft.com/mssql/server:2017-latest",
envVars: new List<string> { "ACCEPT_EULA=Y", "SA_PASSWORD=!MyMagicPasswOrd", "MSSQL_PID=Developer", "CHECK_POLICY=OFF", "CHECK_EXPIRY=OFF" },
portMappings: new Dictionary<string, ISet<string>> { { "1433", new HashSet<string> { "1433" } } }
))
.With<LocalApiService>(() => new DockerServiceSettings("webapi", dockerfilePath: @"../../../../../examples/sample/WebApplication/Dockerfile", tag:"Ab.GG"))
.Build(new DockerInfrastructureSettings(new Uri("npipe://./pipe/docker_engine"), "XunitTestEnv"));
}
public async Task DisposeAsync()
{
await this.containerizedInfrastructure.DisposeAsync();
GC.SuppressFinalize(this);
}
public async Task InitializeAsync()
{
using (var tokenSource = new CancellationTokenSource(60000))
{
await this.containerizedInfrastructure.InitializeAsync(tokenSource.Token).ConfigureAwait(false);
...
}
}
}
- Start creating your integration test.
[Collection("InfrastructureWithExistingRecordsCollection")]
public class AndOtherControllerTest
{
private readonly InfrastructureWithExistingRecords infrastructureWithExistingRecords;
public AndOtherControllerTest(InfrastructureWithExistingRecords infrastructureWithExistingRecords)
{
this.infrastructureWithExistingRecords = infrastructureWithExistingRecords;
}
[Fact]
public async Task GetAndOtherByIdAsync_WithValidId_ReturnsOkWithForecast()
{
// arrange
var id = "1A2B3C";
// act
using (HttpResponseMessage response = await this.infrastructureWithExistingRecords.httpClient.GetAsync($"/AndOther/{id}"))
{
var result = await response.Content.ReadAsAsync<WebApplication.WeatherForecast>();
// assert
response.StatusCode.ShouldBe(HttpStatusCode.OK);
result.Id.ShouldBe(id);
}
}
}
Distributed under the MIT License. See LICENSE.md
for more information.
If you find a bug, or you want to see a functionality in this library, feel free to open an issue.