Mutation Testing Guide

Audience: Stella Ops developers and test automation engineers hardening critical modules.

This guide explains how to run and interpret Stryker.NET mutation testing in the Stella Ops monorepo, and how to close the test gaps it reveals.

Overview

Mutation testing measures test-suite effectiveness by introducing small code changes (mutants) and verifying that tests detect them. Unlike line coverage, mutation testing answers the question that actually matters: “Would my tests catch this bug?”

Per-module baseline and target scores are tracked separately in Mutation Testing Baselines.

Installation

Stryker.NET is configured as a local dotnet tool:

# Restore tools (includes Stryker.NET)
dotnet tool restore

# Verify installation
dotnet stryker --version

Configuration

Solution-Level Configuration

Base configuration is at stryker-config.json in the solution root. Module-specific configs override these settings.

Module Configurations

ModuleConfig PathMutation Break Threshold
Scanner.Coresrc/Scanner/__Libraries/StellaOps.Scanner.Core/stryker-config.json60%
Policy.Enginesrc/Policy/StellaOps.Policy.Engine/stryker-config.json60%
Authoritysrc/Authority/StellaOps.Authority/stryker-config.json65%

Running Mutation Tests

Single Module

# Navigate to module directory
cd src/Scanner/__Libraries/StellaOps.Scanner.Core

# Run mutation testing
dotnet stryker

# With specific config
dotnet stryker --config-file stryker-config.json

All Configured Modules

# From solution root
dotnet stryker --solution StellaOps.Router.slnx

CI Mode (Threshold Enforcement)

# Fails if mutation score below threshold
dotnet stryker --break-at-score 60

Understanding Results

Mutation Score

Mutation Score = (Killed Mutants / Total Mutants) × 100

Thresholds

LevelScoreMeaning
High≥80%Excellent test effectiveness
Low≥60%Acceptable, improvements needed
Break<50%Build fails, critical gaps

Example Output

All mutants have been tested, and your mutation score has been calculated
╔═══════════════════════════════════════════════════════════════════════╗
║ Mutation Testing Report                                                ║
╠═══════════════════════════════════════════════════════════════════════╣
║ Mutants tested: 156                                                    ║
║ Mutants killed: 134                                                    ║
║ Mutants survived: 18                                                   ║
║ Mutants no coverage: 4                                                 ║
║ Mutation score: 85.90%                                                 ║
╚═══════════════════════════════════════════════════════════════════════╝

Common Mutators

MutatorOriginalMutant
Comparison>=>
Equality==!=
Booleantruefalse
Logical&&||
Arithmetic+-
NullCoalescing?? (remove)

Fixing Survived Mutants

1. Analyze the Report

Open the HTML report in .stryker/output/<module>/mutation-report.html.

2. Identify the Gap

Look at the survived mutant:

// Original
if (score >= threshold) { return "PASS"; }

// Mutant (survived!)
if (score > threshold) { return "PASS"; }

3. Add Missing Test

[Fact]
public void Should_Pass_When_Score_Equals_Threshold()
{
    var score = 60;
    var threshold = 60;
    
    var result = EvaluateScore(score, threshold);
    
    result.Should().Be("PASS"); // Now kills the >= to > mutant
}

Best Practices

1. Focus on Critical Modules First

Prioritize mutation testing for:

2. Don’t Chase 100%

Some mutants are false positives or equivalent mutants. Aim for 80%+ on critical modules.

3. Use Baseline Mode

Enable baseline to only test changed files:

dotnet stryker --with-baseline:main

4. Exclude Non-Critical Code

Exclude from mutation testing:

CI Integration

Mutation testing runs in CI:

mutation-test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - name: Run Stryker
      run: |
        dotnet tool restore
        dotnet stryker --break-at-score 60

Troubleshooting

Slow Execution

Out of Memory

Timeout Issues

References