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!