A guide to setting up a .NET Core project using Docker, with integrated unit and component tests.

Joe Honour
12 min readJun 22, 2019

This article aims to show the process of setting up a new .NET core project from scratch, building it within a Docker Image, and showing how you can assure your code with both Unit and Component tests. I also aim to show how integrating your Docker build process with your test process, can lead to easy assurance of your Docker images.

During this guide, we will be using the newest version of .NET Core (3.0.100-preview6) and building a small Web API.

Step 1: Setting up the project structure

To start with, make sure you have the latest version of the .NET Core SDK installed (https://dotnet.microsoft.com/download/dotnet-core/3.0).

With this installed, let’s first create a Web API project. From within your working directory, running the following command.

dotnet new sln -n Example.Service -o .

This will create a solution file in the current directory. In this directory, then create the following file structure shown below (Figure 1).

Figure 1: The project structure from the root directory

The project will be structured as follows:

  • src/Example.Service: this will contain our Web API project.
  • test/Example.Service.UnitTest: this will contain an NUnit test project which will reference the Web API project directly for unit testing.
  • test/Example.Service.ComponentTest: this will contain an NUnit test project which will test the running Web API project at an external level. We can send web requests at our running service, and assert the responses we receive.

To start with, we will create the Web API project. To create this, run the following commands from the project root directory:

# create the project in the correct directory
dotnet new webapi -n Example.Service -o ./src/Example.Service/
# add the project to the solution
dotnet sln Example.Service.sln add ./src/Example.Service/Example.Service.csproj

This will create the Web API project and add it to the overall solution. The Web API contains a simple Values controller that returns some static values. If you open the /src/Example.Service/Controllers/ValuesController.cs file, you will be able to see the default setup the API comes with.

The next step will be to create our unit test project. To do that run the following commands from the project root directory:

# create the unit test project in the correct directory
dotnet new nunit -n Example.Service.UnitTest -o ./test/Example.Service.UnitTest/
# add the unit test project to the solution
dotnet sln Example.Service.sln add ./test/Example.Service.UnitTest/Example.Service.UnitTest.csproj
# add a reference to the web api project within our test project
dotnet add ./test/Example.Service.UnitTest/Example.Service.UnitTest.csproj reference ./src/Example.Service/Example.Service.csproj

This has created the unit test project and added a reference to our Web API project. This means we can import the classes we create within the Web API and run unit tests against them.

Finally, we will create the component test project in a similar way:

# create the component test project in the correct directory
dotnet new nunit -n Example.Service.ComponentTest -o ./test/Example.Service.ComponentTest/
# add the component test project to the solution
dotnet sln Example.Service.sln add ./test/Example.Service.ComponentTest/Example.Service.ComponentTest.csproj

This completes our initial project structure, the next step will be to write a simple unit test and component test that can be run to prove our build process.

Step 2: Adding a simple unit test

To be able to write a simple unit test, we will edit our Web API project to have a service that returns some static values. The Controller can then call into this service to get the values.

Inside the src/Example.Service/ project, create a services folder, so you end up with the project structure found in Figure 2.

Figure 2: Services directory added to the Example.Service Web API project.

Inside of the Services folder, create a new class called ValuesService. This should have the following content:

Figure 3: A simple service that can return static values.

Now we have this service, let’s add a simple unit test to run. Inside the unit test project, there will be a generated UnitTest1.cs file. From within here, add a basic unit test, like the one shown in Figure 4:

Figure 4: A simple unit test for the Values Service.

To run this test, from the /test/Example.Service.UnitTest directory, run the following command:

dotnet test

This should pass, and now the final step we need to do is to make the Web API controller make use of this service. For now, we will just create a static reference to the service, but in a production environment you should use the built in Dependency Injection framework. Given this service exists, edit the values controller to be the following:

Figure 5: The edited values controller. It makes use of the Values Service to return static values.

Now we have the controller using the values service, which we have unit tested, we can add a simple component test that can test the API itself.

Step 3: Adding a simple component test

A component test needs to test the service once it is running. We want to ensure the service behaves as expected in a controlled environment. For our component test, we want to make sure that when we make a GET request, we are returned the correct value. There are a few steps to achieve this, we will need to:

  1. Turn off https redirection on our Web API, so we don’t have to worry about certificates and can make normal HTTP requests when running our component test.
  2. Allow our component tests to read in the URL the Example.Service is running on (for instance, http://localhost:5000) so the tests know where to send the GET request during execution.
  3. Write the actual test that sends the GET request, parses the response, and makes sure it is the list of values we were expecting.

1: Turning off HTTPS redirection

This is relatively simple, just go to the src/Example.Service/Startup.cs file and remove the following line:

app.UseHttpsRedirection();

This will now let us hit the service over HTTP, which makes our testing nice and easy.

2: Reading environment variables in the component tests.

In order to do this, we have a few changes to make to the project. First of all, we need to add a file that can contain our environment variables. These variables can be overridden at runtime when needed.

Add an appsettings.json file to the root of the component test project (test/Example.Service.ComponentTest/). Inside this file, copy the following structure:

{
"ComponentTests": {
"ServiceUri": "http://localhost:5000"
}
}

In order to read this file, and any other environment variables, we need to make some changes to the Example.Service.ComponentTest.csproj file. First, we need to make sure this appsettings.json file is available by adding the following sections to the csproj file:

<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings.json"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>

Along with this, we need to include the Microsoft packages for reading and parsing the values within the file. To do this, add the following package references to the Example.Service.ComponentTest.csproj file:

<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0-preview6.19304.6" /><PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.0.0-preview6.19304.6" /><PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="3.0.0-preview6.19304.6" /><PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0-preview6.19304.6" />

Now we have a file containing the base URL our service will be running on and the packages available to read this in, the final step is to read this value and use it within the tests. To do this, add the following code to the UnitTest1.cs file within the component test project:

Figure 6: Reading in the appsettings.json file for the component test configuration within the UnitTest1.cs file.

You can see we are deserialising the JSON from appsettings.json into a class called ComponentTestConfig. This class exists at the root of the component test project, and has the following structure:

Figure 7: The component test config class. You should add this within your component test project.

Now we have access to everything we need to create our component test. Within the UnitTest1.cs file, add the following test underneath the InitialSetup method:

Figure 8: A simple component test that asserts the result of making a GET request against our Example.Service

In order to run this test, make sure to have the Example.Service Web API running, which can be done by running the following command within the src/Example.Service/ directory.

dotnet run

With the service running, open a second terminal window and navigate to test/Example.Service.ComponentTest/ and run the following command:

dotnet test

You should hopefully see a message that your test has passed.

We now have a simple Web API, a unit test, and a component test. The next stage is to look at how we build and run all these things within Docker.

Step 4: Building and running the Web API in a Docker container

Docker allows you to create lightweight containers, that can package and ship your code with ease across environments. To continue with this article, you will need Docker installed on your machine (https://docs.docker.com/install/).

With Docker installed, we can start to build up our Docker image. To begin with create a Dockerfile in the root of the project.

Figure 9: Adding a Dockerfile to the solution

For the rest of this article, we will be configuring the Dockerfile and I will expect a basic knowledge of Docker. If you are not familiar with Docker, that is fine, and i recommend you have a look over some of the excellent articles available already:

During this, to build the Docker image run the following command from the root of the project:

docker build -t example-service:latest .

To begin with, let’s add the files to our Docker image necessary from our solution to restore the packages the application needs.

FROM mcr.microsoft.com/dotnet/core/sdk:3.0-alpine AS build
WORKDIR /app
# copy sln and csproj files into the image
COPY *.sln .
COPY src/Example.Service/*.csproj ./src/Example.Service/
COPY test/Example.Service.UnitTest/*.csproj ./test/Example.Service.UnitTest/
COPY test/Example.Service.ComponentTest/*.csproj ./test/Example.Service.ComponentTest/
# restore package dependencies for the solution
RUN dotnet restore

If we were to build the docker image now, we would copy over just the csproj and sln files, and restore all necessary packages. This is nice as it means if the csproj/sln files don’t change Docker will cache this layer of the image and not require you to restore your packages every time you make a change to your project.

Now we have the project files and all necessary packages for the application, we can copy over the rest of the files within the project and build the solution.

# copy full solution over
COPY . .
# build the solution
RUN dotnet build

With the solution built, the next thing to do is publish the project, so it runs in release mode when inside the container.

# create a new layer from the build later
FROM build AS publish
# set the working directory to be the web api project
WORKDIR /app/src/Example.Service
# publish the web api project to a directory called out
RUN dotnet publish -c Release -o out

Given we have the application’s published output, we can complete our base docker build by configuring our docker image to run the API when started:

# create a new layer using the cut-down aspnet runtime image
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-alpine AS runtime
WORKDIR /app
# copy over the files produced when publishing the service
COPY --from=publish /app/src/Example.Service/out ./
# expose port 80 as our application will be listening on this port
EXPOSE 80
# run the web api when the docker image is started
ENTRYPOINT ["dotnet", "Example.Service.dll"]

Try running the Web API by using the following commands:

docker build -t example-service:latest .
docker run --rm -it -p 5000:80 example-service:latest

With this running, you can verify the Docker container is running correctly by executing the component tests (follow the instructions from earlier).

This is the basis of creating, and running a Docker image for a .NET core application. However, we still have the problem that our unit tests and component tests are ran separately to the Docker image itself. This brings some issues of:

  • tests might run with a different version of the .NET SDK than the application uses.
  • you can build a Docker image of our service, that doesn't pass unit tests. It’s a nice guarantee to have that if you were able to build the image your unit tests (at least) must have passed.

Let’s look at how we can extend our docker image to not only force the unit tests to run, but be able to run the unit tests from within the Docker image on an ad-hoc basis.

Step 5: Running the unit tests as part of building the Docker Image

To run the unit tests as part of the Docker Image build, we need to add an extra stage to our Dockerfile. Immediately after the section in the Dockerfile where we run:

docker build

Add the following section:

# run the unit tests
FROM build AS test
# set the directory to be within the unit test project
WORKDIR /app/test/Example.Service.UnitTest
# run the unit tests
RUN dotnet test --logger:trx

This will run the unit tests, and if they fail it will fail the Docker build. This means if we build an image, we know that the unit tests must have passed. Along with this, we know they ran on the exact build artefacts that are within the Docker image. There is therefore no chance that the unit tests will pass on one persons machine, but fail on another, or on CI. This gives us a good level of assurance that our service will work when ran.

However, we may also want to run the unit tests separately to building the full Docker image, but we still want them to run within Docker. To do this, we can setup an extra build target within the Dockerfile. Add the following section directly above the new unit test section within the Dockerfile:

# create a new build target called testrunner
FROM build AS testrunner
# navigate to the unit test directory
WORKDIR /app/test/Example.Service.UnitTests
# when you run this build target it will run the unit tests
CMD ["dotnet", "test", "--logger:trx"]

To then build and run the unit tests inside the Docker image, without building the full image, you can run the following commands:

# build to the test target of the Dockerfile
docker build --target testrunner -t example-service-tests:latest .
# run the unit tests
docker run example-service-tests:latest

You should hopefully now see your tests run and pass. So we now have a way to:

  • guarantee all Docker images built have passing unit tests.
  • run the unit tests within Docker, meaning they run directly on the artefacts that are going to make it into the final Docker image.

Given we can build an image with passing unit tests, the final phase is the ability to test the running image with our component tests.

Step 6: Running the component tests from within a Docker container

In order to run the component tests, we want to have the Docker image we have built running. To do this, run the following commands from the root of the project:

docker build -t example-service:latest .
docker run --rm -it -p 5000:80 example-service:latest

We now need a way to run the component tests against this running container. To do this, we will add another build target to our Dockerfile. Immediately after the section in our Dockerfile where we run the unit tests add the following section:

# create a new build target called componenttestrunner
FROM build AS componenttestrunner
# navigate to the component test directory
WORKDIR /app/test/Example.Service.ComponentTest
# when you run this build target it will run the component tests
CMD ["dotnet", "test", "--logger:trx"]

Given we have this, we can then build our Docker image up to this build target, in the same way we run our unit tests.

# build to the test target of the Dockerfile
docker build --target componenttestrunner -t example-service-component-tests:latest .
# run the component tests
docker run --network=host example-service-component-tests:latest

This will run the component tests so they can access services running on the host, meaning our connection string for the service of http://localhost:5000 works correctly.

If you run these commands you should be able to see we can now run our component tests from within a Docker container, and hit our service running inside its own Docker container.

Conclusion: overview of the final solution and Dockerfile

By this point, you should be left with this final Dockerfile:

FROM mcr.microsoft.com/dotnet/core/sdk:3.0-alpine AS build
WORKDIR /app
COPY *.sln .
COPY src/Example.Service/*.csproj ./src/Example.Service/
COPY test/Example.Service.UnitTest/*.csproj ./test/Example.Service.UnitTest/
COPY test/Example.Service.ComponentTest/*.csproj ./test/Example.Service.ComponentTest/
RUN dotnet restore
# copy full solution over
COPY . .
RUN dotnet build
FROM build AS testrunner
WORKDIR /app/test/Example.Service.UnitTest
CMD ["dotnet", "test", "--logger:trx"]
# run the unit tests
FROM build AS test
WORKDIR /app/test/Example.Service.UnitTest
RUN dotnet test --logger:trx
# run the component tests
FROM build AS componenttestrunner
WORKDIR /app/test/Example.Service.ComponentTest
CMD ["dotnet", "test", "--logger:trx"]
# publish the API
FROM build AS publish
WORKDIR /app/src/Example.Service
RUN dotnet publish -c Release -o out
# run the api
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0-alpine AS runtime
WORKDIR /app
COPY --from=publish /app/src/Example.Service/out ./
EXPOSE 80
ENTRYPOINT ["dotnet", "Example.Service.dll"]

We have built the following functionality:

  • The ability to build, publish and run our Web API from within a Docker container.
  • The ability to run unit tests as part of the Docker build process.
  • The ability to run unit tests from within a Docker container on demand.
  • The ability to run component tests from within a Docker container on demand.

I hope this has been useful, and you can see the value to integrating your test strategy with your Docker build process.

If you would like to find out more, please feel free to contact me.

--

--

Joe Honour

Software Engineer, with interests in Distributed Computing.