Ejecutar pruebas de tus contenedores en VSTS

Si desarrollas con Docker es probable que uses multi-stage builds para crear tus contenedores, en este caso unificas bajo un mismo Dockerfile la creación del binario (usando una imagen de compilación) y la creación de la imagen final (basandote en una imagen de runtime).

Ahora bien, si usas un pipeline de CI/CD con VSTS… ¿como gestionar los tests de esos contenedores? Eso es lo que vamos a discutir en este post.

Si tu sistema se compone de un único contenedor en este caso lo más normal puede ser ejecutar las pruebas como parte del proceso de construcción de la imagen final. Es decir, ejecutar las pruebas en el Dockerfile.

Para ello en el proceso de multi-stage simplemente cuando usas la imagen de build ejecutas los tests como un paso más de construcción de la imagen final. Veamos un ejemplo básico de ello. Partamos de dos proyectos: una aplicación web y uno de tests unitarios:

Esquema de la solución

No nos preocupemos por ahora de lo que hace la aplicación web. Por su parte el test unitario prueba el comportamiento del servicio “GuidProvider” y es como sigue:

public class guid_provider_should
{
    [Fact]
    public void never_return_a_empty_guid()
    {
        var provider = new GuidProvider();
        var id = provider.Id;

        id.Should().NotBe(Guid.Empty, "Empty guid can't be returned");
    }
}

Bien, vamos ahora a crear un Dockerfile con multi-stage que me construya la imagen final de WebApplication1 y a la vez que me ejecute los tests:

FROM microsoft/aspnetcore:2.0 AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/aspnetcore-build:2.0 AS build
WORKDIR /src
COPY CiCd.sln ./
COPY WebApplication1/WebApplication1.csproj WebApplication1/
COPY WebApplication1.Tests/WebApplication1.Tests.csproj WebApplication1.Tests/
RUN dotnet restore -nowarn:msb3202,nu1503
COPY . .
WORKDIR /src/WebApplication1
RUN dotnet build -c Release -o /app

FROM build as test
WORKDIR /src/WebApplication1.Tests
RUN dotnet test

FROM build AS publish
WORKDIR /src/WebApplication1
RUN dotnet publish -c Release -o /app

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

Este Dockerfile necesita como contexto de build de Docker el directorio donde está la solución, así pues colocado en este directorio puedes construir la imagen  ejecutando:

docker build -t webapplication1 . -f WebApplication1\Dockerfile

En este caso el test unitario falla (hay un error en GuidProvider y siempre devuelve Guid.Empty) así que docker build fallará con una salida parecida a:

Step 15/22 : RUN dotnet test
 ---> Running in 776cb8501892
Build started, please wait...
Build completed.

Test run for /src/WebApplication1.Tests/bin/Debug/netcoreapp2.0/WebApplication1.Tests.dll(.NETCoreApp,Version=v2.0)
Microsoft (R) Test Execution Command Line Tool Version 15.5.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
[xUnit.net 00:00:01.3504231]   Discovering: WebApplication1.Tests
[xUnit.net 00:00:01.4152529]   Discovered:  WebApplication1.Tests
[xUnit.net 00:00:01.4223592]   Starting:    WebApplication1.Tests
[xUnit.net 00:00:01.7516805]     WebApplication1.Tests.guid_provider_should.never_return_a_empty_guid [FAIL]
[xUnit.net 00:00:01.7545026]       Did not expect id to be {00000000-0000-0000-0000-000000000000} because Empty guid can't be returned.
[xUnit.net 00:00:01.7553789]       Stack Trace:
[xUnit.net 00:00:01.7561760]         C:\projects\fluentassertions-vf06b\Src\FluentAssertions\Execution\XUnit2TestFramework.cs(32,0): at FluentAssertions.Execution.XUnit2TestFramework.Throw(String message)
[xUnit.net 00:00:01.7562630]         C:\projects\fluentassertions-vf06b\Src\FluentAssertions\Execution\AssertionScope.cs(224,0): at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args)
[xUnit.net 00:00:01.7563084]         C:\projects\fluentassertions-vf06b\Src\FluentAssertions\Primitives\GuidAssertions.cs(125,0): at FluentAssertions.Primitives.GuidAssertions.NotBe(Guid unexpected, String because, Object[] becauseArgs)
[xUnit.net 00:00:01.7563496]         /src/WebApplication1.Tests/UnitTests.cs(15,0): at WebApplication1.Tests.guid_provider_should.never_return_a_empty_guid()
[xUnit.net 00:00:01.7751963]   Finished:    WebApplication1.Tests
Failed   WebApplication1.Tests.guid_provider_should.never_return_a_empty_guid
Error Message:
 Did not expect id to be {00000000-0000-0000-0000-000000000000} because Empty guid can't be returned.
Stack Trace:
   at FluentAssertions.Execution.XUnit2TestFramework.Throw(String message) in C:\projects\fluentassertions-vf06b\Src\FluentAssertions\Execution\XUnit2TestFramework.cs:line 32
   at FluentAssertions.Execution.AssertionScope.FailWith(String message, Object[] args) in C:\projects\fluentassertions-vf06b\Src\FluentAssertions\Execution\AssertionScope.cs:line 224
   at FluentAssertions.Primitives.GuidAssertions.NotBe(Guid unexpected, String because, Object[] becauseArgs) in C:\projects\fluentassertions-vf06b\Src\FluentAssertions\Primitives\GuidAssertions.cs:line 125
   at WebApplication1.Tests.guid_provider_should.never_return_a_empty_guid() in /src/WebApplication1.Tests/UnitTests.cs:line 15

Total tests: 1. Passed: 0. Failed: 1. Skipped: 0.
Test Run Failed.
Test execution time: 3.1584 Seconds
The command '/bin/sh -c dotnet test' returned a non-zero code: 1

Ahora veamos como podemos integrar esto en un pipeline de VSTS.

Lo primero es crear nuestra build que tenga, por ahora, una sola tarea de tipo “Docker”:

Es importante que desmarques la casilla de “Use default build context”, y dejes el texto “Build Context” vacío para que así apunte a la raíz del repo (donde tenemos el fichero sln).

Una vez encolada esta falla, ya que falla el test unitario. Hasta ahí todo bien pero no tenemos el resultado de los tests en VSTS. La pestaña tests está vacía ya que por lo que a VSTS respecta no ha habido ejecución alguna de tests:

Vamos a corregir este punto. Para ello debemos cambiar nuestra aproximación y ejecutar los tests en otro contenedor.

Ejecutar los tests como parte de construcción de la imagen no es que esté mal pero eso nos va a impedir que VSTS se entere del resultado. Esto es debido a una “limitación” de Docker que no permite tener volúmenes durante “docker build” por lo que no podemos compartir el fichero de resultados de test (que podemos generar con dotnet test) con VSTS: este fichero se queda en el contenedor intermedio y no podemos sacarlo fácilmente de allí.

Por lo tanto vamos a seguir otra aproximación, ligeramente distinta y es ejecutar los tests en otro contenedor. Por lo tanto vamos a levantar primero un contenedor de tests y ejecutar los tests en él. Lo bueno es que vamos a poder usar el mismo Dockerfile para ambos contenedores. Para ello lo primero es eliminar del Dockerfile la lína que ejecuta el “dotnet test”, ya que ahora lo ejecutaremos aparte.

Con eso se nos guardará el fichero “test-results.xml” con el resultado de los tests en el directorio (/tests/) del contenedor. Vale, ahora vamos a aprovecharnos de una característica de Docker run que es poder ejecutar un Dockerfile no hasta el final si no solo hasta un stage determinado. En nuestro caso este stage es el stage de tests:

docker build -t webapplication1-tests . -f WebApplication1\Dockerfile --target test

Observa el –target test que indica que solo debemos construir hasta este stage. Observa que la imagen generada se llamara “webapplication1-tests”. Esto generará la imagen, y ahora ya podemos ejecutar los tests en ella:

docker run -v/c/tests:/tests  webapplication1-tests --entrypoint "dotnet test --logger trx;LogFileName=/tests/test-results.xml"

Con este docker run ejecutamos la imagen creada en el paso anterior y mediante un volúmen mapeamos el directorio /tests/ del contenedor a un directorio del host (en mi caso c:\tests\). Por lo tanto ahora tengo en c:\tests\ los resultados de los tests.

Si ahora quiero construir la imagen final puedo hacer otro docker build:

docker build -t webapplication1 . -f WebApplication1\Dockerfile

La ventaja es que gracias al modelo de capas de Docker no es necesario re-ejecutar todos los otros stages (es decir, no es necesario recompilar la aplicación).

Bien, vamos ahora a aplicar todo esto a VSTS. Para simplificar la build y evitar meter tantos parámetros nos apoyaremos en compose. Para ello crea el fichero docker-compose.yml que tenga el siguiente contenido:

version: '3.4'

services:
  webapplication1:
    image: webapplication1
    build:
      context: .
      dockerfile: WebApplication1/Dockerfile

  webapplication1-tests:
    image: webapplication1-tests
    build:
      context: .
      dockerfile: WebApplication1/Dockerfile      
      target: test
    volumes: 
      - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests-results/}:/tests

Aquí definimos las dos imágenes (webapplication1 y webapplication1-tests). Nos falta el fichero docker-compose.override.yml:

version: '3.4'

services:
  webapplication1:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "80"
  webapplication1-tests:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "80"
    entrypoint:
      - dotnet
      - test
      - --logger
      - trx;LogFileName=/tests/test-results.xml

Perfecto, ahora para ejecutar los tests nos basta con:

docker-compose run webapplication1-tests

Eso ejecuta los tests (y nos deja el XML de salida en el directorio indicado por la variable de entorno BUILD_ARTIFACTSTAGINGDIRECTORY). Y para construir la imagen final nos basta con:

docker-compose build webapplication1

Ahora ya podemos integrarnos con VSTS. Para ello vamos a crear una build con los siguientes pasos:

  1. Ejecutar docker-compose run webapplication1-tests para ejecutar los tests
  2. Publicar los resultados de los tests
  3. Acciones adicionales (p. ej. construir webapplication1 y hacer el push).

Lo interesante es que si los tests fallan, docker-compose run falla también, lo que hará que la build sea errónea.

Empecemos por la primera tarea, que es una tarea de Docker compose en VSTS configurada como sigue:

Las opciones son a configurar son:

  • Docker compose file: docker-compose.yml
  • Additional docker compose files: docker-compose.override.yml
  • Action: “Run a specific service image”
  • Service Name: webapplication1-tests

La segunda tarea es del tipo “Publish Test Results”:

Las opciones a configurar son:

  • Test Result Format: VSTest
  • Test result files: **/test-results.xml
  • Search folder: $(Build.ArtifactStagingDirectory)
  • Importante: En Control Options -> Run this task -> Seleccionar  “Even if a previous task has failed, unless the build was cancelled”. Esta opción es importante ya que si no nunca se subirían los resultados si los tests fallaran.

A partir de aquí, ya crearías el resto de tareas  (p. ej. construir el resto de servicios y publicarlos en un repositorio) de la forma tradicional.

Ahora una vez ejecutada la build, si los tests fallan la build falla pero ahora vemos los resultados en VSTS:

Tests integrados en VSTS

¡Espero que os haya sido útil!

PD: He dejado un .zip con el código en https://1drv.ms/u/s!Asa-selZwiFlg_Ag_p9batvLwRS5zw (por si quieres echarle un vistazo y probarlo de forma rápida)

3 comentarios sobre “Ejecutar pruebas de tus contenedores en VSTS”

  1. Hi,

    Thanks for this very helpful article.
    However, when trying it into VSTS in can’t make the Test Publish works, got this error :
    No test result files matching **\test-results.xml were found.

    Is there any difference between your article pics and your VSTS CI pipeline ?

  2. Hi,
    The publish step can’t find the xml result file on vsts (I followed exactly your tutorial and used your source code ) is there any additional configuration or extra step not mentioned in your article ?
    Locally when running the docker-compose command the file is created

    Any idea ?
    Thanks

  3. Hi Nicolas,
    The VSTS pipeline I have is exactly the one shown in this article.
    The key point is the bind mount between the test container and the host (the VSTS build agent). Just be sure that you have the volume defined (either in docker-compose.yml as in my post or in docker-compose.override.yml file) and be sure that the path of the bind mount is correct.

    In VSTS I use th BUILD_ARTIFACTSTAGINGDIRECTORY environment variable as a host path for the bind mount, so in the “Publish Test Result Task” I need to set the “Search folder” to Search folder: $(Build.ArtifactStagingDirectory)

    Hope it helps!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *