Docker.Dotnet is a library that you can use to interact with the docker host Remote API. It’s a great library to use if we want to dynamically create containers that need to be controlled within the application. Such as spinning up a local database, RabbitMQ cluster, or an API, while for example, doing integration- and performance-testing.
If you haven’t heard about Docker.Dotnet, great and don’t worry, I will go through it in this post.
You might be familiar with using the Docker CLI to pull down and create images on which you start and create container with.
For example, we can use it to spin up a local Postgres database that our application connects to. We simply open up our terminal and enter this command:
docker run --name some-postgres -e POSTGRES_PASSWORD=mysecretpassword -d postgres
This works very well while we locally test out applications, as they do not interfere with any shared database such as the databases in the dev environment. We may keep it running after the application restarts- it will just be there when we need it.
Things get a bit more tricky when we want it to happen more dynamically…
Let’s say we want to set up the database precisely before we start our integration- or performance-tests. We will have to manually start it before we run and also remove it when the tests are completed, to make sure it’s in a fresh state for each run. Maybe we can have a custom pre-start build event in our csproj that will setup the database through a command line. But that will also decouple the application tests with the database, which will make it harder to troubleshoot and maintain, especially in the CI environment.
If the integration- and performance-test could have full control over its external dependencies and replace them with containers, we would have a perfect environment that does not interfere with anyone, is immutable, and fully customizable for our test scenarios. This is where Docker.Dotnet can help us.
Setting up Docker.Dotnet
As I want to use this in particular for integration- and performance-testing, my idea is to create a library called Docker.Library that my test project can refer to for spinning up external dependencies.
First, we need to download the NuGet package.
Install-Package Docker.DotNet -Version 3.125.4
We then create a new class, I call it DockerHost.cs
public class DockerHost : IAsyncDisposable
{
private readonly DockerClient _dockerClient;
public DockerHost()
{
_dockerClient = new DockerClientConfiguration(new Uri(DockerApiUri())).CreateClient();
}
}
The DockerClientConfiguration takes a Uri which should point to the docker socket. This varies based on the operation system. I extracted it as a private method.
private static string DockerApiUri()
{
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (isWindows)
{
return "npipe://./pipe/docker_engine";
}
var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
if (isLinux)
{
return "unix:///var/run/docker.sock";
}
throw new Exception(
"Default socket location was not found. Please review your docker socket location");
}
Next thing is to start interacting with docker. We need to be able to pull an image if it does not exist.
private async Task PullImageIfNotExist(string image, CancellationToken ct = default)
{
var existingContainers = await _dockerClient.Containers.ListContainersAsync(new ContainersListParameters
{
All = true
}, ct);
var exists = existingContainers.Any(x => x.Image == image);
if (!exists)
{
await _dockerClient.Images.CreateImageAsync(new ImagesCreateParameters
{
FromImage = image,
}, null, null, ct);
}
}
This will list all images and check if the image exists. If not, it will download and create the new image. The default download location is from docker hub. If you want to use a custom docker repository location, you can provide it as one of the CreateImageAsync parameters.
We now need to be able to create a container from the image.
private async Task<string> CreateContainer(string image,string containerName, CancellationToken ct = default)
{
await PullImageIfNotExist(image, ct);
return (await _dockerClient.Containers.CreateContainerAsync(new CreateContainerParameters
{
Image = image,
Name = containerName,
HostConfig = new HostConfig
{
PublishAllPorts = true,
AutoRemove = true
}
}, ct)).ID;
}
And finally we need to start the Container which we created.
public async Task StartContainer(string image,string containerName, CancellationToken ct = default)
{
var containerId = await CreateContainer(image,containerName,ct);
await _dockerClient.Containers.StartContainerAsync(containerId, new ContainerStartParameters(), ct);
}
Using the Docker.Library would look like:
class Program
{
static async Task Main(string[] args)
{
try
{
var dockerHost = new DockerHost();
await dockerHost.StartContainer("postgres", $"integrationtest-{Guid.NewGuid()}");
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
}
This will start a Postgres container.
That concludes setting up a Docker.Library.
The source: https://gist.github.com/raholsn/8ab21851341d91f9bd0c7af947765404
Docker.Dotnet: https://github.com/dotnet/Docker.DotNet
If you would like to take it further by integrating it with your integration-tests. I would recommend using it in conjunction with Microsoft.AspNetCore.Mvc.Testing.
https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1
Happy Coding!