Blog.

CI with Jenkins, MSBuild, Nuget and Git part 3

MF

Marco Franssen /

6 min read1025 words

Cover Image for CI with Jenkins, MSBuild, Nuget and Git part 3

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 a 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.

You have disabled cookies. To leave me a comment please allow cookies at functionality level.

More Stories

Cover Image for Starting with a Node.js webserver

Starting with a Node.js webserver

MF

Marco Franssen /

UPDATE: updated the console output to latest Node.js version and updated the express.js example to latest version. Before starting to explain how you start your first Node.js project for building a simple web server I will first explain you what Node.js is. To do so I just include a quote of the Node.js themself, because I don't like to reinvent the wheel. Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-d…

Cover Image for CI with Jenkins, MSBuild, Nuget and Git part 4

CI with Jenkins, MSBuild, Nuget and Git part 4

MF

Marco Franssen /

In part 1, 2 and 3 I showed you how to create a simple MSBuild script and how to execute it from the command line. We had a look at how to clean your output directories, download the Nuget packages, compile your code, run MSpec tests and creating a code coverage report. In this last part of this series I will show you how to integrate it with Jenkins. First of all we want Jenkins to pull the latest code from Github as soon as somebody has pushed new code to Github. To do this we need to install…

Cover Image for CI with Jenkins, MSBuild, Nuget and Git part 2

CI with Jenkins, MSBuild, Nuget and Git part 2

MF

Marco Franssen /

In part 1 of this blog series we had a look at a very basic MSBuild script which enables us to compile our projects. For doing this I provided you guys with a simple batch file to make it even more simple to execute the build script. In this episode we will focus on cleaning the folders before building again and getting the Nuget packages using Nuget package restore. The latter I will explain further for you first. Nuget package restore can be enabled in Visual studio. A best practice is to en…

Cover Image for CI with Jenkins, MSBuild, Nuget and Git part 1

CI with Jenkins, MSBuild, Nuget and Git part 1

MF

Marco Franssen /

Very lately I have worked on setting up some continuous integration (CI) using MSbuild for my c# project. In this blog series I will explain to you how to set up continuous integration. First of all we will start with creating a MSBuild script which will compile our code. We will also create a small batch file for easy executing the MSbuild script. When that is in place, we will add several targets to the build script to run for example unit tests, integration tests, code coverage, packaging e…