Microsoft активно развивает свои проекты с открытым кодом, например, ASP.NET Core или MSBuild. Вместе с этим набирает популярность и тестовый фреймворк xUnit, используемый в них для модульного тестирования. В этой статье мы рассмотрим несколько способов запуска xUnit-тестов для непрерывной интеграции проекта средствами TeamCity.


Примеры конфигураций сборки можно найти на этом демо-сервере TeamCity, а исходный код лежит в этом репозитории: Lib – это код тестируемого приложения, а Lib.Tests – проект с тестами. Оба этих проекта нацелены на .NET версий net472 и netcoreapp2.1.


Для поддержки xUnit, в тестовом проекте задана NuGet-зависимость на соответствующий пакет xunit:


<PackageReference Include="xunit"/>


Этот мета-пакет не содержит бинарных файлов, а добавляет несколько зависимостей на NuGet-пакеты xunit.core, xunit.assert и xunit.analyzers. Это тестовое API xUnit. Каждый тестовый метод в xUnit помечается атрибутом [Fact] для обычных тестов или [Theory] для параметризованных тестов. Обычно, каждому тестируемому модулю соответствует свой тестовый класс с набором тестовых методов, проверяющих ту или иную логику. Каждому тестируемому проекту соответствует свой тестовый проект.


Когда мы меняем код в рамках непрерывной интеграции проекта, TeamCity автоматически собирает его и проводит модульное тестирование. Чтобы этот процесс заработал, необходимо настроить соответствующую конфигурацию сборки. TeamCity предоставляет множество способов сделать это, но мы рассмотрим лишь некоторые из них. Эти способы перечислены по мере упрощения подхода к тестированию: от настройки всех шагов вручную до самого удобного подхода, при котором TeamCity берет на себя решение большинства задач.


xUnit console runner


В простейшем случае, выполнить тесты можно утилитой xunit.console, из пакета xunit.runner.console. Формат запуска тестов типичен для подобных инструментов: набор тестовых сборок, фильтры для тестов, формат отчета и прочее. Кажущаяся простота использования этого подхода скрывает несколько нюансов:


  1. Где взять xunit.console на агенте TeamCity, чтобы потом использовать его для запуска тестов?
  2. Какую версию xunit.console выбрать? Пакет xunit.runner.console содержит набор исполняемых файлов для разных версий .NET.
  3. Как быть, если нужно выполнить тесты в нескольких сборках одного тестового проекта, созданных для разных версий .NET?
  4. Как настроить сбор статистики покрытия кода? Эта статистика, конечно, не может полностью отражать качество модульного тестирования, но она может быть полезной для обнаружения кода, непокрытого тестами.
  5. Какие параметры использовать для тестовой утилиты и для сбора статистики покрытия кода?
  6. Как передать результаты тестов и статистику покрытия в TeamCity?

Рассмотрим пример конфигурации сборки TeamCity, содержащей 5 шагов, в каждом из которых мы используем ранер .NET:


image


Первым шагом решаем вопрос (1): “Где взять xunit.console?”:


image


Этот шаг использует команду .NET, чтобы добавить зависимость на пакет xunit.runner.console в тестовый проект Lib.Tests. При восстановлении зависимости на шаге 2 утилита xunit.console появится на агенте TeamCity. Если есть несколько тестовых проектов, то зависимость можно будет добавить только в один. Но как определить точный путь к xunit.console после его загрузки? Если ничего не предпринять, пакет будет загружен в стандартную директорию кэша NuGet-пакетов:

  • в Windows: %userprofile%\.nuget\packages
  • на Mac/Linux: ~/.nuget/packages

Эти пути известны, но они зависят от операционной системы, от аккаунта, под которым запущен агент TeamCity, и от персональных настроек среды окружения для этого аккаунта. Условия могут меняться от агента к агенту. Чтобы быть уверенным, по какому пути найдется xunit.console, лучше задать переменную среды окружения NUGET_PACKAGES со значением %teamcity.build.checkoutDir%/packages. Эта переменная определяет, где появятся NuGet-пакеты после восстановления зависимостей на следующем шаге сборки. В этом примере она указывает на произвольную директорию packages, относительно корневой директории проекта. Вот как это выглядит на странице редактирования параметров:


image


Благодаря этой переменной окружения, путь к xunit.console больше не зависит от внешних факторов. Следующий шаг довольно прост. Он строит решение (solution), восстанавливая зависимости:


image


После его выполнения, в директорию packages добавятся NuGet-пакеты всех зависимостей, включая xunit.runner.console, а в директорию Lib.Tests/bin/Debug – тестовые сборки, соответствующие целевым версиям .NET. И если версия тестовой сборки в директории Lib.Tests/bin/Debug/net472 уже готова для выполнения тестов, то директория Lib.Tests/bin/Debug/netcoreapp2.1 для .NET CoreApp 2.1 не содержит всех требуемых бинарных зависимостей. Вместо этого, в ней присутствуют _JSON-_файлы с описанием того, где найти эти бинарные зависимости. Шаг 3 собирает всё вместе для приложений .NET CoreApp 2.1:


image


После выполнения этого шага можно подвести промежуточный итог: тесты готовы к запуску. Тестовые сборки со всеми зависимостями находятся в директориях:


  • Lib.Tests/bin/Debug/net472
  • Lib.Tests/bin/Debug/netcoreapp2.1/publish

Необходимые для запуска тестов утилиты xunit.console соответственно находятся в:


  • packages/xunit.runner.console/**/net472/xunit.console.exe
  • packages/xunit.runner.console/**/netcoreapp1.0/xunit.console.dll

где ** – версия пакета xunit.runner.console.


Вопросы (1) и (2) решены. Для решения вопроса (3) необходимо добавить два шага, выполняющих тесты для двух версии .NET. Потенциально, количество целевых версий тестовых проектов .NET может быть довольно большим, поэтому и шагов тестирования с похожим набором параметров тоже может быть много. Эту проблему можно решить, например, с помощью PowerShell-скрипта или TeamCity Kotlin DSL. С вопросами (4) и (5), в общем случае, приходится разбираться самостоятельно, но, использовав команду .NET, мы получим следующие преимущества:

  • статистику покрытия кода с передачей параметров, кроссплатформенностью и всеми отчетами
  • автоматический запуск xunit.console.dll и _xunit.console.exe _подходящим способом, в зависимости от выбранного окружения (ОС, Docker, и т.д.)

Следующие два шага выполняют тесты командой .NET:

image


image


Открытым остался последний вопрос (6): “Как передать результаты тестов TeamCity?”. xunit.console делает это самостоятельно, полагаясь на переменную среды окружения _TEAMCITY_PROJECTNAME, которую агент TeamCity автоматически добавляет ко всем порожденным процессам. xunit.console передает результаты тестов, используя TeamCity service messages.


Хотя все вопросы и решены, но было бы здорово, если бы настройка тестов не занимала столько времени и усилий. В какой-то момент энтузиасты попытались уменьшить количество шагов конфигурации, используя механизм TeamCity Meta-Runner.


Meta-Runners Power Pack


Пакет TeamCity мета-ранеров Power Pack содержит мета-ранер xUnit.net-dotCover, который упрощает запуск xUnit-тестов и сбор статистики покрытия кода. Пример конфигурации сборки с его использованием содержит всего два шага:


image


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


image


Этот шаг получает xunit.console из того же NuGet-пакета xunit.runner.console и запускает тесты сборок только для полных версий .NET Framework (в нашем случае .NET Framework 4.72), попутно собирая статистику покрытия кода. Он заменяет 2 шага скачивания xunit.console и запуска тестов по сравнению с предыдущим подходом.


Недостатки мета-ранера xUnit.net-dotCover:


  • Не может запускать тесты в тестовых проектах, собранных для .NET Core и .NET 5+.
  • Пользовательский интерфейс для передачи параметров dotCover не очень нагляден.
  • Нужно самостоятельно выбирать версию xunit.console в поле Xunit Runner Executable.

Очевидно, что мета-ранер не подходит для нашего случая, но, тем не менее, является рабочим решением для тестовых проектов, нацеленных на полные версии .NET Framework.


dotnet test


.NET Runner с командой test является самым простым, надежным и мощным способом тестировать .NET код в TeamCity. Наша задача решается всего лишь одним простым шагом конфигурации:


image


Такой подход имеет следующие преимущества:


  • Он не зависит от фреймворков тестирования: xUnit, NUint и других. Можно использовать и несколько одновременно.


  • Тесты могут выполняться для всех тестовых сборок решения или нескольких решений, для одного или нескольких проектов.


  • Можно запускать тесты для определенной версии .NET или для набора версий в многоцелевых проектах с использованием элемента TargetFrameworks, включая Full .NET Framework, .NET Core и .NET 5+.


  • Поддерживается тестирование в Docker-контейнерах.


  • Кросс-платформенный сбор статистики покрытия кода.



Если тестовый проект создан в средах разработки Visual Studio или Rider или с использованием шаблонов из командной строки dotnet new, например, dotnet new xunit -o Lib.Tests, то ничего дополнительного делать не нужно. Если же тестовый проект создается в "блокноте", то, помимо зависимости xunit, дополнительно нужно добавить зависимость на пакет Microsoft.NET.Test.Sdk и на тестовый адаптер xunit.runner.visualstudio:


<PackageReference Include="Microsoft.NET.Test.Sdk"/>


<PackageReference Include="xunit.runner.visualstudio"/>


Пакет Microsoft.NET.Test.Sdk содержит набор свойств и скриптов MSBuild, которые делают проект тестовым, а тестовый адаптер отвечает за интеграцию определенного тестового фреймворка: в нашем случае xunit.runner.visualstudio, с Visual Studio Test Platform. Другие фреймворки также имеют свои адаптеры, например, NUnit – NUnit3TestAdapter, а MSTest – MSTest.TestAdapter.


Мы рекомендуем использовать именно этот подход для тестирования вместе с xUnit и другими тестовыми фреймворками.


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