Введение
Последнее время активно пропагандируется практика разработки программного обеспечения Test-Driven Development. Бесспорно, она очень полезна, но не всегда и не все ее применяют. Поэтому часть кода покрыта юнит-тестами, а часть остается непокрытой. Проследить за каждым проектом вручную, нормально написаны там тесты или нет, является практически невыполнимой задачей.
Недавно я задался вопросом, а как можно автоматизировать процесс сбора метрики, которая показывает процент покрытия кода тестами. Было решено встроить ее сбор в CruiseControl.NET. Естественно, что 100% покрытие не гарантирует отсутствие багов, но хотя бы показывает отношение разработчиков к написанию тестов.
В данной статье я не буду останавливаться на моментах касающихся настройки CruiseControl’а для сборки проектов и запуска юнит-тестов. Будут описаны шаги, которые позволят собрать необходимую информацию о покрытии кода и вывести ее на странице с тестами. Для написания юнит-тестов наша компания использует фреймворк от Microsoft – MSTest. Результаты работы с этим фреймворком и будут описываться в статье. Стоит заметить, что необходимым условием для встраивания покрытия кода является изначальная настройка CruiseControl'а для запуска тестов.
Насколько я знаю, CruiseControl имеет встроенные возможности по отображению данных собранных при помощи NCover. Но по причине того, что купить эту библиотеку у нас нет возможности, мы и пользуемся теми методами, которые есть.
Всё нижеописанное относится к проектам, написанным в Visual Studio 2010. Отличия с 2008 версией хоть и небольшие, но они есть.
Создание data.coverage файла на билд-сервере
Visual Studio позволяет настроить выполнение тестов таким образом, чтобы после прохождения всех тестов создавался файл, в котором будет храниться информация о покрытых участках кода. Называться этот файл будет data.coverage (хранится он в папке In с результатами прохождения тестов).
Эта настройка хранится в файле .testrunconfig вашего проекта. Через UI ее можно выставить следующим образом:
- Откройте ваш файл .testrunconfig
- Выберите пункт Data and Diagnostics
- Выставите флажок напротив Code Coverage и нажмите Configure
- Выберите библиотеку, для которой необходимо собирать покрытие
- Выключите флажок Instrument assemblies in place
После сохранения файла внутри него будет следующий текст:
<?xml version="1.0" encoding="UTF-8"?>
<TestSettings name="Local Test Run" id="de0d45b4-4fed-4acb-a663-2cfdf0ce4fd7" xmlns="http://microsoft.com/schemas/VisualStudio/TeamTest/2010">
<Description>This is a default test run configuration for a local test run.</Description>
<Deployment enabled="false" />
<Execution>
<Timeouts testTimeout="300000" />
<TestTypeSpecific>
<UnitTestRunConfig testTypeId="13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b">
<AssemblyResolution>
<TestDirectory useLoadContext="true" />
</AssemblyResolution>
</UnitTestRunConfig>
</TestTypeSpecific>
<AgentRule name="LocalMachineDefaultRole">
<DataCollectors>
<DataCollector uri="datacollector://Microsoft/CodeCoverage/1.0" assemblyQualifiedName="Microsoft.VisualStudio.TestTools.CodeCoverage.CoveragePlugIn, Microsoft.VisualStudio.QualityTools.Plugins.CodeCoverage, PublicKeyToken=b03f5f7f11d50a3a" friendlyName="Code Coverage">
<Configuration>
<CodeCoverage xmlns="">
<Regular>
<CodeCoverageItem binaryFile="YourProject\bin\Debug\YourProject.dll" pdbFile="YourProject\bin\Debug\YourProject.pdb" />
</Regular>
</CodeCoverage>
</Configuration>
</DataCollector>
</DataCollectors>
</AgentRule>
</Execution>
</TestSettings>
Секция DataCollectors как раз и говорит MSTest'у, что необходимо собирать Code Coverage.
Теперь мы знаем, что должно быть указано в конфиге, и у нас есть 3 варианта его применения на билд-сервере:
- использовать готовый файл конфига из вашего проекта;
- хранить в проекте отдельный конфиг для билд-сервера;
- создавать файл конфига на лету.
Первый кейс нам не подходит, т.к. этот файл хранится в системе контроля версий, поэтому любой разработчик может выключить настройку по сбору Code Coverage и положить этот файл назад. Или наоборот, если мы, например, не хотим в некоторых случаях собирать эту метрику. По этим причинам настройка о том, собирать или не собирать метрику должна быть включена в CruiseControl. Второй вариант также не позволяет полностью предотвратить проблему описанную выше. По вышеописанным причинам мы у себя в компании остановились на третьем варианте. Описывать способ создания XML файла я не буду, там всё достаточно просто.
Когда конфиг готов можно запускать тесты, для этого в конфиг файл CruiseControl'а в секцию с тасками необходимо добавить следующий код:
<exec>
<executable>C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\MSTest.exe</executable>
<buildArgs>"/testcontainer:PathToWorkingDirectory\Source\bin\Release\YourTestProject.dll" "/runconfig:PathToYourConfig\localtestrun.testrunconfig" "/resultsfile:PathToWorkingDirectory\TestResults.xml"</buildArgs>
<description>Executing MSTest</description>
</exec>
Если запустить сейчас CruiseControl, то файл data.coverage будет создан в папке PathToWorkingDirectory\[USERNAME]_[MACHINE-NAME] [DATE AND TIME]\In\[MACHINE-NAME]. Т.к. в названии папки присутствует время, то в дальнейшем будет не очень удобно использовать ее. Поэтому в .testrunconfig необходимо добавить следующую секцию (внутрь <TestSettings>):
<NamingScheme baseName="FolderName" appendTimeStamp="false" useDefault="false" />
Теперь файл data.coverage можно будет найти в папке PathToWorkingDirectory\FolderName\In\[MACHINE-NAME].
После проделанных действий в папке с результатами тестов для этого проекта на машине с CruiseControl’ом будет находиться файл data.coverage. Проблема в том, что этот файл бинарный, поэтому нам необходимо сконвертировать его в xml.
Конвертация data.coverage в XML
Для конвертации этого файла необходимо написать консольное приложение, которое будет выполняться после прохождения всех тестов. Тут всё достатчно просто, код который осуществляет конвертацию представлен ниже:
using (CoverageInfo info = CoverageInfo.CreateFromFile(PathToDataCoverageFile))
{
CoverageDS data = info.BuildDataSet();
string outputXml = data.GetXml();
File.WriteAllText(PathToOutputXmlCoverageFile,outputXml));
}
В выходном XML будет очень много информации, которая нам не нужна, поэтому можно применить XSL-преобразование и оставить только те секции, которые нам необходимы. Также имена методов будут иметь полное название включая namespace, что несколько неудобно при отображении детальной информации о покрытии, и это решается простой модификацией получившегося XML файла.
Запуск нашего консольного приложения осуществляется благодаря дополнительной секции в конфиге билд-сервера:
<exec>
<executable>PathToConverter\Coverage2XmlConverter.exe</executable >
<buildArgs>"PathToDataCoverage\data.coverage" "PathToOutputXml\coverage.xml"</buildArgs>
<description>Calculation code coverage data</description>
</exec>
Добавление данных к результатам билда
Теперь у нас почти всё готово. Достаточно добавить получившийся XML файл к результатам билда. Делается это достаточно просто, необходимо добавить следующую секцию в конфиг CruiseControl'а внутрь секции Publishers:
<merge>
<files>
<file>PathToResults\TestResult.xml</file>
<file>PathToCoverage\coverage.xml</file>
</files>
</merge>
Файл TestResult.xml уже должен был быть у вас в конфиге, если у вас настроена работа с тестами.
Отображение данных на странице с билдом
Осталось написать файл XSLT для отображения собранных данных на страницу с билдом. На билдах, в которых покрытие составляет меньше 20%, показатель Code Coverage будет красным, в случае 20%-50% показатель будет желтым и в случае >50% он будет зеленым. Эти значения легко можно поменять в XSLT, и использовать те, которые больше вас устраивают.
Общее значение покрытия мы показываем на странице с билдом, вот как теперь это выглядит:
Для этого пришлось немного модифицировать MSTestReport2008.xsl из стандартного набора CruiseControl.NET.
А так выглядят детализация Code Coverage метрики:
Занимать место в статье текстом XSLT я не буду, поэтому выложил на GitHub 2 XSLT-файла, вместе с приложением для конвертации data.coverage файла в XML.
Спасибо большое, буду раз замечаниям и дополнениям.