Integration testing using a docker container

Integration tests sometimes rely on external resources. It can be a database server like Microsoft SQL Server, PostgreSQL or CouchDB, an external service like GitLab or Artifactory, a SSH server, etc. When you run a test that use such a service, you need to ensure you start with a clean state. This is where Docker is useful. Docker allows to quickly spawn a new instance of a service. You can run a new docker instance at the beginning of the test and stop it at the end. This way your tests always run in the same context.

Let's see how you can create a test that use GitLab! In my case I don't want to create a new container each time, because it takes about 1 minute to spawn the container. Instead, I reuse the existing container if possible. This is useful on my developer machine to not spend too much time waiting for the container. However, on the CI server, the docker will be spawn each time for consistency.

Note: I assume you have already installed Docker on your computer

To use Docker from a .NET application you can use the NuGet package Docker.DotNet (NuGet, GitHub). This library allows you to download an image, start a new container or to list the containers and get their state.

First, you need to create a client to communicate with the Docker service.

using (var conf = new DockerClientConfiguration(new Uri("npipe://./pipe/docker_engine"))) // localhost
using (var client = conf.CreateClient())
{
}

Then, check if the container exists. If not, download the docker image and start the container:

const string ContainerName = "GitLabTests";
const string ImageName = "gitlab/gitlab-ee";
const string ImageTag = "latest";

var containers = await client.Containers.ListContainersAsync(new ContainersListParameters() { All = true });
var container = containers.FirstOrDefault(c => c.Names.Contains("/" + ContainerName));
if (container == null)
{
    // Download image
    await client.Images.CreateImageAsync(new ImagesCreateParameters() { FromImage = ImageName, Tag = ImageTag }, new AuthConfig(), new Progress<JSONMessage>());

    // Create the container
    var config = new Config()
    {
        Hostname = "localhost"
    };

    // Configure the ports to expose
    var hostConfig = new HostConfig()
    {
        PortBindings = new Dictionary<string, IList<PortBinding>>
        {
            { "80/tcp", new List<PortBinding> { new PortBinding { HostIP = "127.0.0.1", HostPort = "8080" } },
        }
    };

    // Create the container
    var response = await client.Containers.CreateContainerAsync(new CreateContainerParameters(config)
    {
        Image = ImageName + ":" + ImageTag,
        Name = ContainerName,
        Tty = false,
        HostConfig = hostConfig,
    });

    // Get the container object
    containers = await client.Containers.ListContainersAsync(new ContainersListParameters() { All = true });
    container = containers.First(c => c.ID == response.ID);
}

Then, you have to ensure the container is running. This is in the case you are reusing an existing container:

// Start the container is needed
if (container.State != "running")
{
    var started = await client.Containers.StartContainerAsync(container.ID, new ContainerStartParameters());
    if (!started)
    {
        Assert.Fail("Cannot start the docker container");
    }
}

Finally, you have to ensure the service is actually running. Indeed, the container can be running but the service may take a few seconds to start. The logic here is very dependent on the kind of service you are using. For instance, for GitLab you need to test the home page is responding.

using (var httpClient = new HttpClient())
{
    while (true)
    {
        try
        {
            using (var response = await httpClient.GetAsync("http://localhost:8080"))
            {
                if (response.IsSuccessStatusCode)
                    break;
            }
        }
        catch
        {
        }
    }
}

You can now use the container to run your test. If your are using MSTest, you can use this code in the AssemblyInitializeAttribute to start the container because the tests run.

[TestClass]
public class Initialize
{
    private static string _containerId;

    [AssemblyInitialize]
    public static void AssemblyInitialize(TestContext context)
    {
        StartContainer.Wait();
    }

    private static async Task StartContainer()
    {
        // code omitted for brevity
    }
}

At the end, you can stop the container using the AssemblyCleanup attribute.

[AssemblyCleanup]
public static void AssemblyCleanup()
{
    client.Containers.StopContainerAsync(container.ID, new ContainerStopParameters()).Wait();
    client.Containers.RemoveContainerAsync(container.ID, new ContainerRemoveParameters { Force = true }).Wait();
}

That's all folk! You'll find a complete example on my .NET GitLab client on GitHub.

Leave a reply