Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí
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:
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:
- Ejecutar docker-compose run webapplication1-tests para ejecutar los tests
- Publicar los resultados de los tests
- 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:
¡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)