Die Ausführung von Unit Tests im Buildprozess kann komfortabel über Testlisten verwaltet werden. In manchen Fällen ist das aber nicht anwendbar, z.B. wenn zu viele Solutions gebaut werden und die Tests in Form von Test-Assemblies vorliegen. Hierbei sollen alle Tests ausgeführt werden, die im Source Ordner eines Projektes stecken. Gleichzeitig sollen Code Coverage Werte ermittelt werden.
Dabei treten folgende Fragestellungen auf:
-
Wie führe ich MSTest in einem lokalen Build aus?
-
Wie ermittle ich alle Test Assemblies?
-
Wie aktiviere ich Code Coverage?
Dieser Beitrag soll die Antworten aufzeigen.
Tests im lokalen Build
Um MSTest ausführen zu können, wird der TestToolsTask verwendet, der steckt im Assembly Microsoft.VisualStudio.QualityTools.MSBuildTasks.dll
Um ihn einzubinden wird folgende Statement im MSBUild Skript hinterlegt:
<UsingTask
TaskName="TestToolsTask"
AssemblyFile="$(VS90COMNTOOLS)\..\IDE\PrivateAssemblies\Microsoft.VisualStudio.QualityTools.MSBuildTasks.dll"
Condition="Exists(‘$(VS90COMNTOOLS)\..\IDE\PrivateAssemblies\Microsoft.VisualStudio.QualityTools.MSBuildTasks.dll’)"/>
Der Task akzeptiert wie MSTest.exe die Übergabe von TestContainern. In unserem Fall werden die TestAssemblies als TestContainer verwendet. Außerdem akzeptiert der task die Übergabe eine Test Run Configuration, welche notwendig ist um Code Coverage zu aktivieren.
Test Assemblies zusammenstellen
Um alle Test Assemblies zu ermittlen, hilft uns eine klare Regel zur Namensgebung. Im vorliegenden Fall existiert folgende Namensregel:
[ProduktName].[ModulName].[Test|Tests].dll, also z.B. AIT.DataLogger.Tests.dll
Anhand dieser Regel lassen sich sehr schnell alle Test-Assemblies ermitteln. Im vorliegenden Fall kommt erleichternd hinzu, dass alle Assemblies nach dem Build in einem zentralen Ordner liegen. Aus diesem können sowohl die test Assemblies als auch die Code Coverage Assemblies ermittelt werden.
Das Target zur Ausführung der Tests sieht dann in etwa so aus:
<Target Name="RunAllTests" DependsOnTargets="PrepareTestRunConfig">
<!– First get all test assemblies–>
<CreateItem Include="$(MSBuildProjectDirectory)\Bin\*.Test.dll">
<Output TaskParameter="Include" ItemName="TestAssemblies"/>
</CreateItem>
<CreateItem Include="$(MSBuildProjectDirectory)\Bin\*.Tests.dll">
<Output TaskParameter="Include" ItemName="TestAssemblies"/>
</CreateItem>
<TestToolsTask TestContainers="@(TestAssemblies)" RunConfigFile="$(LocalTestRunConfigFile)"/>
<CallTarget Targets="CleanTestTemp" />
</Target>
Code Coverage aktivieren
Um Code Coverage zu aktivieren, wird ein Test Run Configuration File benötigt. In diesem werden die Einstellungen zur Ausführung der Tests festgehalten. Hierzu zählen unter anderem die Benamung des Testlaufs sowie die Einstellung der Codeabdeckung. Das File ist ein XML-basiert, lässt sich aber auch mit dem WriteLinesToFile-Task sehr einfach erzeugen:
<WriteLinesToFile File="$(LocalTestRunConfigFile)" Lines="@(Header)" Overwrite="true" />
<WriteLinesToFile
File="$(LocalTestRunConfigFile)"
Lines="<CodeCoverageItem binaryFile="$(MSBuildProjectDirectory)\Bin\%(CodeCoverageAssemblies.FileName)%(CodeCoverageAssemblies.Extension)" pdbFile="$(MSBuildProjectDirectory)\Bin\%(CodeCoverageAssemblies.FileName).pdb" />" />
<WriteLinesToFile File="$(LocalTestRunConfigFile)" Lines="@(Footer)" />
Wir setzen unser TestRunConfig dabei aus 3 Teilen zusammen, einem festen Header, den ermittelten Code Coverage Assemblies und einem festen Footer. Header und Footer sind fest definiert, können also direkt im BuildFile angegeben werden oder aber aus Input-Files gelesen werden.Wir Nutzen Input-Files um das Build-Skript so sauber wie möglich zu halten.
Für die Assemblies, für die Code Coverage ermittelt werden soll, werden Einträge der Form <CodeCoverageItem binaryFile=“file“ pdbFile=“file“ /> erzeugt. Hierzu wurden im Vorfeld alle zu berücksichtigenden Assemblies ermittelt. Auch hier half uns wieder die Namensgebungsregel.
Wir berücksichtigen alle Dateien, die [ProduktName].* lauten. Da hier auch noch die Test Assemblies drunter fallen, ist ein wenig Mehraufwand notwendig, um die die richtige Menge an Assemblies zu ermitteln. Im vorliegenden Fall lösen wir dies durch kopieren in einen separierten Bereich und gezieltes Löschen.
Das Target hierfür sieht wie folgt aus:
<Target Name="PrepareCodeCoverageFolder">
<!–
Steps to perform:
1. Create a temp folder
2. Copy all files from bin to temp folder
3. Delete all files that are not taken into account for code coverage
–>
<!– 1. Make directory. Ensure that it is empty –>
<RemoveDir Directories="$(CodeCoverageTempDir)" Condition="Exists(‘$(CodeCoverageTempDir)’)" />
<MakeDir Directories="$(CodeCoverageTempDir)" />
<CreateItem Include="$(MSBuildProjectDirectory)\Bin\AIT*">
<Output TaskParameter="Include" ItemName="BinRheoAssemblies"/>
</CreateItem>
<Copy SourceFiles="@(BinAssemblies)" DestinationFolder="$(CodeCoverageTempDir)" />
<CreateItem Include="$(CodeCoverageTempDir)\*.Test.dll">
<Output TaskParameter="Include" ItemName="CodeCoverageExcludeAssemblies"/>
</CreateItem>
<CreateItem Include="$(CodeCoverageTempDir)\*.Tests.dll">
<Output TaskParameter="Include" ItemName="CodeCoverageExcludeAssemblies"/>
</CreateItem>
<CreateItem Include="$(CodeCoverageTempDir)\*.xml">
<Output TaskParameter="
Include" ItemName="CodeCoverageExcludeAssemblies"/>
</CreateItem>
<Delete Files="@(CodeCoverageExcludeAssemblies)" />
</Target>
Schlussendlich genügt dann ein einfach msbuild script.target /t:RunAllTests um alle Tests auszuführen. Der TestToolsTask erzeugt einen Ordner TestResults, in dem sich die Ergebnisse finden. Das erzeugte *.trx File lässt sich im Visual Studio öffnen und zeigt die Testergebnisse samt Code Coverage: