Using Docker for compiling your code is great since that guarantees a consistent behaviour regardless of where you are building your code, if it’s on the local dev machine or on a build server somewhere. It also reduces the need of installing any dependencies just to make the code compile, the only thing that you need is Docker.
When you create a ASP.NET Core project in Visual Studio and add Docker support for it you will get a Docker file that looks something like this:
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM microsoft/dotnet:2.1-sdk AS build WORKDIR /src COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"] RUN dotnet restore "WebApplication1/WebApplication1.csproj" COPY . . WORKDIR "/src/WebApplication1" RUN dotnet build "WebApplication1.csproj" -c Release -o /app FROM build AS publish RUN dotnet publish "WebApplication1.csproj" -c Release -o /app FROM base AS final WORKDIR /app COPY --from=publish /app . ENTRYPOINT ["dotnet", "WebApplication1.dll"]
This is an example of a multistage Docker build. The first stage is based on the .NET Core SDK Docker image in which the code is restored, built and published. The second phase uses the smaller .NET Core runtime Docker image, to which the generated artifacts from the first phase is copied into.
This results in a smaller Docker image that will be pushed to a Docker registry and later on deployed onto testing and production environments. Smaller images means faster download and startup times, but also since it doesn’t contain as many SDKs etc the surface area for security holes is typically smaller.
Now, this will compile just fine locally, and settting a build definition in Azure Pipelines is easy-peasy. Using the default Docker container build pipeline template, results in a build like this:
But, we want to run unit tests also, and then publish the test results back to Azure DevOps. How can we do this?
First of all we need to build and run the tests inside the container, so we need to extend the Docker file. In this sample, I have added a XUnit test project called WebApplication1.UnitTests.
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM microsoft/dotnet:2.1-sdk AS build WORKDIR /src COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"] COPY ["WebApplication1.UnitTests/WebApplication1.UnitTests.csproj", "WebApplication1.UnitTests/"] RUN dotnet restore "WebApplication1/WebApplication1.csproj" RUN dotnet restore "WebApplication1.UnitTests/WebApplication1.UnitTests.csproj" COPY . . RUN dotnet build "WebApplication1/WebApplication1.csproj" -c Release -o /app RUN dotnet build "WebApplication1.UnitTests/WebApplication1.UnitTests.csproj" -c Release -o /app RUN dotnet test "WebApplication1.UnitTests/WebApplication1.UnitTests.csproj" --logger "trx;LogFileName=webapplication1.trx" FROM build AS publish RUN dotnet publish "WebApplication1.csproj" -c Release -o /app FROM base AS final WORKDIR /app COPY --from=publish /app . ENTRYPOINT ["dotnet", "WebApplication1.dll"]
Now we are also restoring and compiling the test project, and then we run dotnet test to actually run the unit tests. Since we want to later on be able to publish the results of the unit tests to Azure DevOps, we are using the –logger parameter to specify that dotnet test should output a TRX file and we also give it a name for clarity.
Now comes the tricky part. When we run these tests as part of a build, the results end up inside the container. To be able to publish the test results we need to access this file from outside the container. Locally we could have used Docker volumes to do this, but this will not work on a hosted build server.
Instead we will add another task to our build definition that will use scripts to build the image, including running the unit tests, and the copiying the test results file from the container to a folder on the build server. We use the Docker Copy command to do this:
docker build -f ./WebApplication1/Dockerfile --target build -t webapplication1:$(build.buildid) . docker create -ti --name testcontainer webapplication1:$(build.buildid) docker cp testcontainer:/src/WebApplication1.UnitTests/TestResults/ $(Build.ArtifactStagingDirectory)/testresults docker rm -fv testcontainer
Here we first build the image by using docker build. By using the –target parameter it will only execute the first phase of the build (there is no meaning to continue if the tests are failing). To be able to access the file inside the container, we use docker create which is a way to create and configure a container before actually starting it. In this case we don’t need to start it, just use docker cp to extract the test result files to the host.
Now we will have the TRX test results file in the artifact folder on the build server, which means we can just add a Publish Test Results task to our build definition:
And voila, running the build now runs the unit tests and we can see the test results in the build summary as expected: