Integrate CI with Nuke and Stryker

Hamed Shirbandi
4 min readOct 25, 2023

--

Struggling to improve code quality and test coverage in your development process? In this article, we’ll reveal how integrating CI with Nuke and Stryker can be your solution!

Before we dive in, be sure to check out my two other articles on Nuke and Stryker for a more in-depth understanding. These resources can complement what we’ll cover here.

Building a real-world example

In this hands-on journey, we’ll apply the principles of CI with Nuke and Stryker to a real-world project on GitHub named TaskoMask. For an in-depth understanding, feel free to explore the project itself. And if you find it valuable, a star would be much appreciated :)

Preparing the Build Plan

To start, let’s create a build plan. For TaskoMask, I’ve made a plan to make sure the code always gets compiled, check for any bugs with unit tests, and ensure the quality of these tests with mutation testing. Here’s the plan:

Nuke-build-plan

Implementing the Build Plan with Nuke

Here is the build implementation using Nuke. You can also find the latest version on GitHub.

class Build : NukeBuild
{
public static int Main() => Execute<Build>(x => x.RunMutationTests);

[Parameter]
readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;

[Solution]
readonly Solution Solution;

[Parameter]
AbsolutePath TestResultDirectory = RootDirectory + "/.nuke/Artifacts/Test-Results/";

Target LogInformation => _ => _
.Executes(() =>
{
Log.Information($"Solution path : {Solution}");
Log.Information($"Solution directory : {Solution.Directory}");
Log.Information($"Configuration : {Configuration}");
Log.Information($"TestResultDirectory : {TestResultDirectory}");
});

Target Preparation => _ => _
.DependsOn(LogInformation)
.Executes(() =>
{
TestResultDirectory.CreateOrCleanDirectory();
});

Target RestoreDotNetTools => _ => _
.Executes(() =>
{
DotNet(arguments: "tool restore");
});

Target Clean => _ => _
.DependsOn(Preparation)
.Executes(() =>
{
DotNetClean();
});

Target Restore => _ => _
.DependsOn(Clean)
.Executes(() =>
{
DotNetRestore(a => a.SetProjectFile(Solution));
});

Target Compile => _ => _
.DependsOn(Restore)
.Executes(() =>
{
DotNetBuild(a =>
a.SetProjectFile(Solution)
.SetNoRestore(true)
.SetConfiguration(Configuration));
});

Target RunUnitTests => _ => _
.DependsOn(Compile)
.Executes(() =>
{
var testProjects = Solution.AllProjects.Where(s => s.Name.EndsWith("Tests.Unit"));

DotNetTest(a => a
.SetConfiguration(Configuration)
.SetNoBuild(true)
.SetNoRestore(true)
.ResetVerbosity()
.SetResultsDirectory(TestResultDirectory)
.EnableCollectCoverage()
.SetCoverletOutputFormat(CoverletOutputFormat.opencover)
.SetExcludeByFile("*.Generated.cs")
.EnableUseSourceLink()
.CombineWith(testProjects, (b, z) => b
.SetProjectFile(z)
.AddLoggers($"trx;LogFileName={z.Name}.trx")
.SetCoverletOutput(TestResultDirectory + $"{z.Name}.xml")));
});

Target RunMutationTests => _ => _
.DependsOn(RunUnitTests,RestoreDotNetTools)
.Executes(() =>
{
//It will add dashboard reporter
string reporter = "--reporter dashboard";

//we don't use dashboard reporter on local builds
if (IsLocalBuild)
reporter = "";

var testProjects = Solution.AllProjects.Where(s => s.Name.EndsWith(".Tests.Unit"));

foreach (var testProject in testProjects)
DotNet(workingDirectory: testProject.Directory, arguments: $"stryker {reporter}");
});
}

The implementation is smooth and straightforward. Each step in the build plan is implemented as a target. Additionally, you’ll discover how we can incorporate Stryker for mutation testing into our Nuke build targets.

Integrating with GitHub Actions workflow (CI)

Here is the CI implementation using GitHub Actions. You can also find the latest version on GitHub.

name: build

env:
Configuration: Release
ContinuousIntegrationBuild: true

on:
pull_request:
branches: ["master"]
push:
branches: ["master"]

jobs:
build:
runs-on: windows-latest

steps:
- uses: actions/checkout@v3
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: 6.0.x

- name: Build
run: ./build.ps1 Compile

- name: Unit Test
run: ./build.ps1 RunUnitTests --skip Compile

- name: Mutation Test
run: ./build.ps1 RunMutationTests --skip RunUnitTests
env:
STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}

I assume you are familiar with GitHub Actions or other CI tools, so let’s talk about the last three steps (Build, Unit Test, Mutation Test).

Nuke generates some build scripts that you can use in your CI pipelines. Here, we’re using the build.ps1 script, which is placed in the root of the repository. For each step, we run the build.ps1 script and pass the desired target name as its argument.

Build Step

In the Build step, we run the Compile target which depends on Restore, Clean, Preparation and LogInformation targets. So it runs all of those targets one by one based on their priority.

Unit Test Step

In this step, because the RunUnitTests target depends on Compile target, we don’t need to run the Compile target again, we skip it by passing — skip argument.

Mutation Test Step

We need to skip unit tests and also provide STRYKER_DASHBOARD_API_KEY to be able to send the mutation testing result to Stryker dashboard.

On TaskoMask, I’ve configured Stryker to break the build if the mutation score falls below the threshold specified in stryker-config.json. For more details, check out my discussion on this topic in my other article.

--

--

Hamed Shirbandi

Over 8 years of experience in Software Development, passionate about DDD, TDD, BDD, Microservices, OOP, Design Patterns and Principles