Click here to Skip to main content
Click here to Skip to main content

Tagged as

Go to top

CI with Jenkins, MSBuild, Nuget and Git: Part 3

, 6 May 2013
Rate this:
Please Sign up or sign in to vote.
I will explain to you how to create an MSpec MSBuild target and a Code coverage MSBuild target.

In the previous parts (part 1, part 2) of this series I described how to clean, download Nuget packages and build your solution using MSBuild. In this part I will explain you how to create an MSpec MSBuild target and a Code coverage MSBuild target.

MSpec is a testing framework which enables you to write living specifications. Using the MSpec console runner you can easily generate an html report. This report can later on be published using the Jenkins report plugin. By publishing this report we have some documentation on the specifications of the software available and because we did write the specifications using MSpec we also have unit tests in place. So that’s why I call it living documentation. :D

For generating the code coverage report we use the XML output of our MSpec tests. These reports will also be published using the Jenkins report plugin. To do so I use OpenCover and ReportGenerator.

All three packages are installed in my solution using Nuget. So the paths in my build script are based on the paths of my source/packages folders.

MSpec MSBuild target

First we add some variables/properties to our build script for easier access to the msbuild tools. In the MSpec target we first scan our directory for all assemblies containing ‘Specs’ in their name. So my Specification projects will be called something like this:

  • MyProject.Domain.Specs.csproj –> outputs MyProject.Domain.Specs.dll
  • MyProject.Service.Specs.csproj –> outputs MyProject.Service.Specs.dll

The last thing to mention about the MSpec MSBuild target is about the quotes needed. In MSBuild we can encode the code using ". I encoded all needed quotes as you can see in the command definition.

<PropertyGroup>
  <!-- MSpec -->
  <MSpecPath>$(Packages)\Machine.Specifications.0.5.12\tools</MSpecPath>
  <MSpecExe>mspec-clr4.exe</MSpecExe>
  <MSpecXmlOutputFile>$(ReportsPath)\mspec-output.xml</MSpecXmlOutputFile>
  <MSpecHtmlOutputPath>$(ReportsPath)\mspec</MSpecHtmlOutputPath>
  <MSpecSettings></MSpecSettings>
</PropertyGroup>
<Target Name="Specs" DependsOnTargets="Clean;LoadNuGetPackages;Compile">
  <CreateItem Include="**\bin\$(Configuration)\*Specs*.dll" 
                    Exclude="**\bin\$(Configuration)\*Specs*.mm.dll">
    <Output TaskParameter="Include" ItemName="SpecsAssemblies" />
  </CreateItem>
  <PropertyGroup>
    <MSpecCommand>&quot;$(MSpecPath)\$(MSpecExe)&quot; $(MSpecSettings) --xml 
      &quot;$(MSpecXmlOutputFile)&quot; --html &quot;$(MSpecHtmlOutputPath)&quot; 
      -t &quot;@(SpecsAssemblies, '&quot; &quot;')&quot;</MSpecCommand>
  </PropertyGroup>
  <Message Importance="high" Text="Running Specs with this command: $(MSpecCommand)"/>
  <Exec Command="$(MSpecCommand)" />
</Target>

Oh. The exclude for the Specs*.mm.dll is because I use ContinuousTests as plugin in my Visual Studio. It runs my specs when I save my files in Visual Studio. Because I also run the msbuild on my local machine I want to exclude these generated assemblies from the build task.

Code coverage MSBuild target

For generating code coverage we also add some variables/properties to our MSBuild script. Again I scan for all the spec assemblies which should be used to generate the coverage. We use MSBuild as target to execute the Specifications and we exclude all Spec assemblies from the coverage report using the $(OpenCoverFilter) property. Based on the XML output of OpenCover we use Report Generator to generate an HTML report and a xml summary with the coverage results of our solution.

<PropertyGroup>
  <!-- OpenCover -->
  <!-- The tools path for OpenCover -->
  <OpenCoverPath>$(Packages)\OpenCover.4.5.1403</OpenCoverPath>
  <OpenCoverExe>OpenCover.Console.exe</OpenCoverExe>
  <OpenCoverFilter>-[*Specs*]* +[*]*</OpenCoverFilter>
  <ReportGeneratorPath>$(Packages)\ReportGenerator.1.8.1.0</ReportGeneratorPath>
  <ReportGeneratorExe>ReportGenerator.exe</ReportGeneratorExe>
  <OpenCoverOutputFile>$(ReportsPath)\coverage-output.xml</OpenCoverOutputFile>
  <CoverageReport>$(ReportsPath)\coverage</CoverageReport>
  <ReportGeneratorSummary>$(ReportsPath)\coverage-summary.xml</ReportGeneratorSummary>
</PropertyGroup>
<Target Name="CodeCoverage" DependsOnTargets="Clean;LoadNuGetPackages;Compile">
  <CreateItem Include="**\Bin\Debug\*Specs*.dll" 
            Exclude="**\Bin\$(Configuration)\*Specs*.mm.dll">
    <Output TaskParameter="Include" ItemName="SpecsAssemblies" />
  </CreateItem>
  <PropertyGroup>
    <OpenCoverCommand>&quot;$(OpenCoverPath)\$(OpenCoverExe)&quot; -register:user 
       &quot;-target:&quot;$(MSpecPath)\$(MSpecExe)&quot;&quot; 
       &quot;-targetargs:&quot;@(SpecsAssemblies, '&quot; &quot;')&quot;&quot; 
       &quot;-filter:$(OpenCoverFilter)&quot; 
       &quot;-output:$(OpenCoverOutputFile)&quot;</OpenCoverCommand>
    <ReportGeneratorCommand>&quot;$(ReportGeneratorPath)\$(ReportGeneratorExe)&quot; 
      &quot;-reports:$(OpenCoverOutputFile)&quot; &quot;-targetdir:$(CoverageReport)&quot; 
      &quot;-reporttypes:html;xml&quot;</ReportGeneratorCommand>
  </PropertyGroup>
  <Message Importance="high" 
     Text="Running code coverage with this command: $(OpenCoverCommand)"/>
  <Exec Command="$(OpenCoverCommand)" />
  <Message Importance="high" 
     Text="Generate report with this command: $(ReportGeneratorCommand)"/>
  <Exec Command="$(ReportGeneratorCommand)" />
  <!-- Report Generator has no way to name the output file so rename it 
                    by copying and deleting the original file -->
  <Copy SourceFiles="$(CoverageReport)\Summary.xml" 
     DestinationFiles="$(ReportGeneratorSummary)"></Copy>
  <Delete Files="$(CoverageReport)\Summary.xml"></Delete>
</Target>

Of course we could have chosen to put the scanning for assemblies outside the targets so both targets can use the same output. However I choose to make them dedicated to the target, because I want my targets to be completely independent. So when I choose to change the included assemblies for my coverage this doesn’t influence my other target. However you are free to share the scanning part in both targets.

So how does our complete MSBuild script look now. To execute the targets we can use the /t:Specs or the /t:Coverage command line parameter. Or to do both /t:Specs;Coverage. You could add some if statements, to be able to choose the target, to the batch file from part 1.

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"
         DefaultTargets="Compile">
  <PropertyGroup>
    <Configuration>Debug</Configuration>
    <Platform>AnyCPU</Platform>
    <DefineSolutionProperties>false</DefineSolutionProperties>
 
    <!-- General Paths -->
    <RootPath>$(MSBuildProjectDirectory)</RootPath>
    <SrcPath>$(RootPath)\src</SrcPath>
    <ReportsPath>$(RootPath)\reports</ReportsPath>
    <ToolsPath>$(RootPath)\tools</ToolsPath>
    <Packages>$(SrcPath)\packages</Packages>
    <!-- MSpec -->
    <MSpecPath>$(Packages)\Machine.Specifications.0.5.12\tools</MSpecPath>
    <MSpecExe>mspec-clr4.exe</MSpecExe>
    <MSpecXmlOutputFile>$(ReportsPath)\mspec-output.xml</MSpecXmlOutputFile>
    <MSpecHtmlOutputPath>$(ReportsPath)\mspec</MSpecHtmlOutputPath>
    <MSpecSettings></MSpecSettings>
    <!-- OpenCover -->
    <!-- The tools path for OpenCover -->
    <OpenCoverPath>$(Packages)\OpenCover.4.5.1403</OpenCoverPath>
    <OpenCoverExe>OpenCover.Console.exe</OpenCoverExe>
    <OpenCoverFilter>-[*Specs*]* +[*]*</OpenCoverFilter>
    <ReportGeneratorPath>$(Packages)\ReportGenerator.1.8.1.0</ReportGeneratorPath>
    <ReportGeneratorExe>ReportGenerator.exe</ReportGeneratorExe>
    <OpenCoverOutputFile>$(ReportsPath)\coverage-output.xml</OpenCoverOutputFile>
    <CoverageReport>$(ReportsPath)\coverage</CoverageReport>
    <ReportGeneratorSummary>$(ReportsPath)\coverage-summary.xml</ReportGeneratorSummary>
  </PropertyGroup>
  
  <!-- The Clean Target -->
  <ItemGroup>
    <ProjectFiles Include="**\*.csproj" />
  </ItemGroup>
  <Target Name="Clean">
    <Message Importance="high" Text="Cleaning folders"/>
    <RemoveDir Directories="$(ReportsPath)" Condition="Exists('$(ReportsPath)')" />
    <MakeDir Directories = "$(ReportsPath);$(ReportsPath)\MSpec;$(ReportsPath)\Coverage" />
    <!-- Clean the source code projects -->
    <MSBuild Projects="@(ProjectFiles)"
             ContinueOnError="false"
             Targets="Clean"
             Properties="Configuration=$(Configuration)" />
  </Target>
  
  <!-- The LoadNuGetPackages Target -->
  <ItemGroup>
    <NuGetPackageConfigs Include="$(MSBuildStartupDirectory)\**\packages.config" />
  </ItemGroup>
  <Target Name="LoadNuGetPackages">
    <Message Importance="high" Text="Retrieving packages for %(NuGetPackageConfigs.Identity)" />
    <Exec Command="&quot;$(SrcPath)\.nuget\nuget&quot; install 
      &quot;%(NuGetPackageConfigs.Identity)&quot; -o &quot;$(SrcPath)\packages&quot;" />
  </Target>
  
  <!-- The Compile Target -->
  <Target Name="Compile" DependsOnTargets="Clean;LoadNuGetPackages">
    <Message Importance="high" Text="Compiling core projects"/>
    <MSBuild Projects="$(SrcPath)\MyProject.Core\MyProject.Core.csproj"
             Properties="Configuration=$(Configuration);Platform=$(Platform)" />
    <MSBuild 
        Projects="$(SrcPath)\MyProject.Web\MyProject.Web.csproj;
                          $(SrcPath)\MyProject.Win\MyProject.Win.csproj"
        Properties="Configuration=$(Configuration);Platform=$(Platform)"
        BuildInParallel="true" />    
  </Target>
  <Target Name="Specs" DependsOnTargets="Clean;LoadNuGetPackages;Compile">
    <CreateItem Include="**\bin\$(Configuration)\*Specs*.dll" 
               Exclude="**\bin\$(Configuration)\*Specs*.mm.dll">
      <Output TaskParameter="Include" ItemName="SpecsAssemblies" />
    </CreateItem>
    <PropertyGroup>
      <MSpecCommand>&quot;$(MSpecPath)\$(MSpecExe)&quot; $(MSpecSettings) --xml 
        &quot;$(MSpecXmlOutputFile)&quot; --html &quot;$(MSpecHtmlOutputPath)&quot; 
        -t &quot;@(SpecsAssemblies, '&quot; &quot;')&quot;</MSpecCommand>
    </PropertyGroup>
    <Message Importance="high" Text="Running Specs with this command: $(MSpecCommand)"/>
    <Exec Command="$(MSpecCommand)" />
  </Target>
  <Target Name="CodeCoverage" DependsOnTargets="Clean;LoadNuGetPackages;Compile">
    <CreateItem Include="**\Bin\Debug\*Specs*.dll" 
               Exclude="**\Bin\$(Configuration)\*Specs*.mm.dll">
      <Output TaskParameter="Include" ItemName="SpecsAssemblies" />
    </CreateItem>
    <PropertyGroup>
      <OpenCoverCommand>&quot;$(OpenCoverPath)\$(OpenCoverExe)&quot; -register:user 
         &quot;-target:&quot;$(MSpecPath)\$(MSpecExe)&quot;&quot; 
         &quot;-targetargs:&quot;@(SpecsAssemblies, '&quot; &quot;')&quot;&quot; 
         &quot;-filter:$(OpenCoverFilter)&quot; 
         &quot;-output:$(OpenCoverOutputFile)&quot;</OpenCoverCommand>
      <ReportGeneratorCommand>&quot;$(ReportGeneratorPath)\$(ReportGeneratorExe)&quot; 
        &quot;-reports:$(OpenCoverOutputFile)&quot; &quot;-targetdir:$(CoverageReport)&quot; 
        &quot;-reporttypes:html;xml&quot;</ReportGeneratorCommand>
    </PropertyGroup>
    <Message Importance="high" 
      Text="Running code coverage with this command: $(OpenCoverCommand)"/>
    <Exec Command="$(OpenCoverCommand)" />
    <Message Importance="high" 
       Text="Generate report with this command: $(ReportGeneratorCommand)"/>
    <Exec Command="$(ReportGeneratorCommand)" />
    <!-- Report Generator has no way to name the output file so rename 
                it by copying and deleting the original file -->
    <Copy SourceFiles="$(CoverageReport)\Summary.xml" 
        DestinationFiles="$(ReportGeneratorSummary)"></Copy>
    <Delete Files="$(CoverageReport)\Summary.xml"></Delete>
  </Target>
</Project>

In the next part of this series we will see how to let Jenkins do all this cool stuff for us. So please be patient and in the meantime share the first three parts of this series with your friends.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

marcofranssen
Software Developer Atos
Netherlands Netherlands
I am a .NET Software Developer at Atos International. Architecture, CQRS, DDD, C#, ASP.NET, MVC3, HTML5, Jquery, WP7, WPF, ncqrs, Node.js
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
-- There are no messages in this forum --
| Advertise | Privacy | Mobile
Web01 | 2.8.140916.1 | Last Updated 6 May 2013
Article Copyright 2013 by marcofranssen
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid