Sometimes you might see them call integration tests but either way, if you are using Docker to its fullest, then you don't want to install Dotnet on your build servers just to run functional tests against a Docker container with your app installed and fortunately, you don't have to.

The Test project

You can create whatever type of test project you want but I use NUnit but also with the amazing WebApplicationFactory class from the Microsoft.AspNetCore.Mvc.Testing nuget package, which allows you to run the app inside an in-memory server. For basic integration tests or calls to APIs, this is all you really need.

The WebApplicationFactory is simply a generic class so you will need to specify the type of Startup from your app under test but otherwise you simply create a new one and then call CreateClient() on it to return an HttpClient. Note that the factory and client are disposable so best to create them in OneTimeSetup and dispose them explicitly in OneTimeTearDown, although you can also create them per test (which is slower but might be cleaner).

Other than that, you simply call client.GetAsync("/api/version") or whatever and get a response that you can test. You can test razor pages and all sorts but I am only testing an API currently. Note that functional tests are not to test all variations of logic that you should try and test in unit tests (which are generally much faster) but instead to test high level journeys for both correctness and speed!

The Docker File

There are a couple of problems with the docker file produced by Visual Studio. Firstly, you should make sure that it is at a level above any folders it will need access to. For example, my test projects are siblings to my projects under test so the dockerfile needs to be moved up a level from the project. It is ok to have multiple docker files in the same directory but you will need to modify the build process accordingly! I think it also affects Visual Studio's ability to do the docker debugging so you might want a duplicate in the project folder without the test changes!

My file looks like this (note I am running on ubuntu): EDIT 11/11/2019 - added publish step to Dockerfile

FROM AS base

FROM AS build
COPY ["Microservices.Templates/Microservices.Templates.csproj", "Microservices.Templates/"]
RUN dotnet restore "Microservices.Templates/Microservices.Templates.csproj"
COPY . .
WORKDIR "/src/Microservices.Templates"
RUN dotnet publish "Microservices.Templates.csproj" -c Release -o /app

FROM build AS testrunner
RUN dotnet restore "Tests/Functional/Functional.Templates.Tests/Functional.Templates.Tests.csproj"
RUN dotnet build
ENTRYPOINT ["dotnet", "test", "--logger:trx"]

FROM build AS publish

WORKDIR "/src/Microservices/Templates"
RUN dotnet publish "Microservices.Templates.csproj", -c Release -o /app

FROM base AS final
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Microservices.Templates.dll"]

The ordering is slightly weird since some versions of dotnet seem to put the base and the build in different orders but anyway...

 The bold part is the important part. Up until then it is just a normal publish. The test layer has a name that we will invoke in the build server "testrunner" and all we then do is restore and then build the tests project and specify that when this container is run, it will run dotnet test and sets the logger format accordingly.

The cool thing with Docker builds is that subsequent builds will reuse any layers that are already built so when we build the deployment after testing, none of the previous layers will need rebuilding!

The Build Process

The build process is fairly simple, first we build the docker file using --target testrunner to ensure it only goes that far. We can also tag it with test so we can easily target it in the test step.

Executing the tests starts with creating a directory for test results and then runs our docker container with a volume mapping to the test results directory so that we can keep the results after the container exists (docker run --rm -v "$(pwd)"/TestResults:/app/tests/TestResults microservices.templates:test)

If this step fails then the build will fail with errors in the console (I haven't yet wired it up to Team City proper), otherwise we are good to continue.

In the final build step, we simply build without specify a target which builds every layer but due to the cache, my small API build only took 2 seconds!


The documentation is a bit all over the place for each of these but once you have it, you should fairly easily understand what is going on. Enjoy.