Integration testing using a docker container

 
 
  • Gérald Barré

Integration tests sometimes rely on external resources, such as a database server like Microsoft SQL Server, PostgreSQL, or CouchDB, an external service like GitLab or Artifactory, or an SSH server. When running a test that depends on such a service, you need to ensure you start from a pristine state. This is where Docker comes in handy. Docker lets you quickly spin up a new instance of a service. You can start a new Docker container at the beginning of a test and stop it at the end, ensuring your tests always run in a consistent environment.

Let's see how to write a test that uses GitLab! In this example, I avoid creating a new container each time because it takes about 1 minute to start. Instead, I reuse an existing container when possible. This saves time on a developer machine, while on a CI server a fresh container is started each time for consistency.

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

To interact with Docker from a .NET application, you can use the Docker.DotNet NuGet package (NuGet, GitHub). This library lets you download images, start containers, list containers, and check their state.

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

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

Then, check whether the container already exists. If not, download the Docker image and create the container:

C#
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);
}

Next, ensure the container is running. This step is relevant when reusing an existing container:

C#
// 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 need to wait for the service inside the container to be ready. Even when the container is running, the service itself may take a few seconds to start. The logic for this varies depending on the service. For GitLab, for example, you can poll the home page until it responds successfully.

C#
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 in your tests. If you are using MSTest, place this code in a method decorated with AssemblyInitializeAttribute to start the container before the tests run.

C#
[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
    }
}

To clean up, stop and remove the container using the AssemblyCleanup attribute.

C#
[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! You can find a complete example in my .NET GitLab client on GitHub.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?