Skip to content

To-Do-Later/containerized-infrastructure

Repository files navigation

Contributors Forks Stargazers Issues MIT License


Containerized Infrastructure

A simple builder for creating your integration test infrastructure.
Explore the docs »

Samples · Request Feature

About The Project

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.

NuGet Nuget

Roadmap

  • 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).

Getting Started

Explore the examples

Prerequisites

Add the package to your project

  • Package Manager
    PM> Install-Package ToDoLater.Containerized.Infrastructure.Docker
  • PackageReference
    <PackageReference Include="ToDoLater.Containerized.Infrastructure.Docker"/>

Setup

  1. Create a class which inherits from DockerContainerizedService.
  2. Override the IsReadyAsync to define how the check for readiness should be done.
  3. 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",

       ...
    };
}
  1. 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);
            ...
        }
    }
}
  1. 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);
        }
    }
}

License

Distributed under the MIT License. See LICENSE.md for more information.

MIT License

Feedback

If you find a bug, or you want to see a functionality in this library, feel free to open an issue.