С приходом .Net Core у нас появилась прекрасная возможность не только запускать наш код на разных ОС, но и тестировать его на разных ОС. А что может быть лучше Docker при работе с разными ОС?

Тестирование более ценно, когда у вас нет никакой разницы между тестовой средой с целевыми средами. Представьте, что вы поддерживаете свое приложение на нескольких операционных системах или версиях операционной системы. С помощью Docker вы можете протестировать свое приложение в каждой из них.

В этой статье мы рассмотрим, как создать отдельный образ, в котором будут запускаться юнит тесты вашего приложения и настроить для всего этого CI/CD пайплайн в VSTS который Azure DevOps с недавних пор.

Если вы работаете с Docker, вероятно, вы используете многоэтапные (multi-stage builds) сборки для создания своих контейнеров. В таком случае вы объединяете создание бинарников (используя build образ) и создание финального образа (используя runtime образ) в рамках одного и того же Docker-файла.

Если ваша система состоит из одного контейнера, в этом случае наиболее обычным подходом может быть запуск тестов как часть процесса построения финального образа. То есть, запуск тестов в Dockerfile.

Чтобы сделать это в многоэтапном процессе, при запуске docker build вы выполняете тесты как еще один шаг в построении финального образа. Давайте посмотрим на простой пример. Допустим у нас есть два проекта: веб-приложения и юнит тесты:


Пока не будем беспокоиться о том, что делает веб-приложение. С другой стороны, у нас есть единственный тест, который проверяет поведение GuidProvider и выглядит следующим образом:

[Fact]
public void Never_return_a_empty_guid()
{
     // Arrange & Act
     var provider = new GuidProvider();
     var id = provider.Id;

     // Assert
     Assert.NotEqual(Guid.Empty, id);
}

Теперь создадим Dockerfile, который будет создавать образ WebApplication и в то же время запускать тесты:

FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/dotnet:2.1-sdk AS build
WORKDIR /src

COPY CiCd.sln .
COPY WebApplication/WebApplication.csproj WebApplication/
COPY WebApplication.Test/WebApplication.Test.csproj WebApplication.Test/
RUN dotnet restore

COPY . .
WORKDIR /src/WebApplication
RUN dotnet build --no-restore -c Release -o /app

FROM build as test
WORKDIR /src/WebApplication.Test
RUN dotnet test

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

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

Этот Dockerfile нужно поместить в каталог с солюшн файлом (СiCd.sln). Для создания образа используем команду:

docker build -t webapplication .

Наш тест терпит неудачу (ошибка в GuidProvider которая всегда возвращает Guid.Empty), поэтому сборка образа завершится неудачно:

output

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

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

Starting test execution, please wait...
[xUnit.net 00:00:00.96]     WebApplication.Test.GuidProviderTests.Never_return_a_empty_guid [FAIL]
Failed   WebApplication.Test.GuidProviderTests.Never_return_a_empty_guid
Error Message:
 Assert.NotEqual() Failure
Expected: Not 00000000-0000-0000-0000-000000000000
Actual:   00000000-0000-0000-0000-000000000000
Stack Trace:
   at WebApplication.Test.GuidProviderTests.Never_return_a_empty_guid() in /src/WebApplication.Test/GuidProviderTests.cs:line 17

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

Теперь давайте посмотрим, как запустить этот процесс в Azure DevOps.

Наш build definition на данный момент представляет собой одну задачу типа «Docker»:



В итоге запуска, билд фейлится, потому что у нас падает тест. Кроме того, у нас нет результатов выполнения тестов (вкладка «Тест» пуста), так как не выполняется тестирование в понимании VSTS:



Запускать тесты как часть сборки образа не то чтобы совсем плохо, но это будет препятствовать тому, чтобы VSTS узнал о результате выполнение. Это связано с «ограничением» Docker, который не позволяет создавать тома во время docker build, поэтому мы не можем предоставить файл c результатами тестов (который можно сгенерировать с помощью dotnet test), этот файл остается в промежуточном контейнере, и мы не можем легко достать его оттуда.

Мы будем придерживаться другого подхода и воспользуемся отличной альтернативой docker run. Сначала поднимем отдельный контейнер и будем запустить тесты в нем. Для обоих контейнеров мы сможем использовать один и тот же Dockerfile. Для начала прежде всего нужно удалить строку, которая запускает dotnet test из Dockerfile, поскольку теперь мы будем запускать их отдельно. Хорошо, теперь давайте воспользуемся командой docker run, которая позволяет запускать Dockerfile до определенного этапа. В нашем случае это — этап тестирования:

docker build -t webapplication-tests .  --target test

Параметр -target указывает какой этап нужно собрать. Обратите внимание, что сгенерированный образ будет иметь название "webapplication-tests". Теперь можно запустить наши тесты и при этом сохранять файл "test-results.trx" с результатами их выполнения в каталоге "tests" контейнера:

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

Здесь мы запускаем образ, созданный на предыдущем шаге, и через том сопоставляем каталог "tests" контейнера с каталогом хоста (в моем случае D:\CiCD\tests). В итоге, у меня в D:\CiCD\tests появились результаты тестов.

Для того чтобы построить финальный образ запускаем:

docker build -t webapplication . 

Преимущество заключается в том, что благодаря модели уровней Docker, нет необходимости повторно выполнять все остальные этапы (то есть нет необходимости перекомпилировать приложение).

Ну, давайте теперь применим все это к Azure DevOps pipelines. Чтобы упростить сборку и избежать большого количества параметров, мы будем использовать docker-compose. Наш docker-compose.yml имеет следующий контент:

version: '3.5'

services:

  webapplication:
    image: webapplication
    build:
      context: .
      dockerfile: Dockerfile

  webapplication-tests:
    image: webapplication-tests
    build:
      context: .
      dockerfile: Dockerfile      
      target: test

Здесь мы определяем два образа (webapplication и webapplication-tests). Чтобы все было по канону, давайте добавим файл docker-compose.override.yml:

version: '3.5'

services:

  webapplication:
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
    ports:
      - "8080:80"

  webapplication-tests:
    entrypoint:
      - dotnet
      - test
      - --logger
      - trx;LogFileName=/tests/test-results.trx
    volumes: 
      - ${BUILD_ARTIFACTSTAGINGDIRECTORY:-./tests/}:/tests

Отлично, теперь для запуска тестов нам просто нужно:

docker-compose run webapplication-tests

Эта команда запускает тесты и создает выходной trx-файл в каталоге, указанном переменной среды BUILD_ARTIFACTSTAGINGDIRECTORY или используется дефолтное значение ./tests. Финальный образ делаем так:

docker-compose build webapplication

Теперь можно отредактировать наш CI процесс в Azure DevOps. Для этого определим следующие шаги:

  1. Собрать все образы [build]
  2. Запустить юнит тесты [run]
  3. Опубликовать результат выполнения тестов [publish]
  4. Запушать образы в хранилище (Registry) [push]

Начнем с первого шага, который представляет собой таск(задачу) Docker Compose в Azure:



Ставим Action: Build service images и указываем путь к docker-compose.yml.

Дальше запускаем контейнер с юнит тестами:



Здесь надо выбрать Action: Run a specific service image и указать имя контейнера Service Name: webapplication-tests. Также, не забываем о пути к docker-compose.yml и docker-compose.override.yml. Значение для Run in Background должно быть не установлено, в противном случае контейнер будет запускаться в «Detached mode» и задача не будет ожидать результатов выполнения тестов а перейдет к следующему шагу. Задача «Publish Test Results» будет пытаться опубликовать результаты, которых может еще не быть, так как запуск тестов занимает определенное время.

Третий шаг — «Опубликовать результаты тестов»:



Важно указать Run this task: Even if a previous task has failed, unless the build was canceled. Этот параметр важен, поскольку в противном случае результаты никогда не будут публиковаться, если тесты не прошли. Search folder: $(Build.ArtifactStagingDirectory)

Последний шаг будет пушать образы в хранилище. Для этого нужно указать Azure подписку, а также Azure Container Registry. Все готово, для того чтобы создать новый билд. Сохраняем. Запускаем. Если тесты не проходят, билд завершится неудачно, но теперь мы видим результаты в VSTS:



Надеюсь, данный материал был полезным. Мой yml файл с конфигурацией сборки вы можете найти здесь.

Спасибо за внимание!

Комментарии (0)