Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Mutation testing

# Stryker.NET on the pure split math (SplitCalculator): the only unit-testable
# logic that is fully DB-free, so the job is fast and hermetic (no SQL Server).
#
# Runs when the mutated code, its tests or the config change; nightly-style on a
# schedule; and on demand. Fails when the mutation score drops below the `break`
# threshold in stryker-config.json.
on:
pull_request:
branches: [main]
paths:
- 'SplitIt.API/SplitIt.Infrastructure/Services/SplitCalculator.cs'
- 'SplitIt.Tests/SplitCalculator*'
- 'stryker-config.json'
- '.github/workflows/mutation.yml'
schedule:
- cron: '0 4 * * 1'
workflow_dispatch:

permissions:
contents: read

jobs:
stryker:
name: Stryker.NET (SplitCalculator)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- name: Install Stryker.NET
run: dotnet tool install --global dotnet-stryker

- name: Restore
run: dotnet restore SplitIt.API/SplitIt.Back.sln

- name: Run mutation tests
run: dotnet stryker --config-file stryker-config.json

- name: Upload mutation report
if: always()
uses: actions/upload-artifact@v4
with:
name: mutation-report
path: StrykerOutput/**/reports/
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,6 @@ Desktop.ini

# SonarScanner for .NET working directory
.sonarqube/

# Mutation testing output (Stryker.NET)
StrykerOutput/
59 changes: 59 additions & 0 deletions SplitIt.Tests/SplitCalculatorBoundaryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using SplitIt.Infrastructure.Services;

namespace SplitIt.Tests;

/// <summary>
/// Boundary and validation cases for the split math, complementing
/// <see cref="SplitCalculatorTests"/> so mutation testing has fewer survivors.
/// </summary>
public class SplitCalculatorBoundaryTests
{
[Fact]
public void EqualSplit_ZeroTotal_Throws() =>
Assert.Throws<ArgumentException>(() => SplitCalculator.EqualSplit(0m, new[] { 1, 2 }));

[Fact]
public void EqualSplit_NullParticipants_Throws() =>
Assert.Throws<ArgumentException>(() => SplitCalculator.EqualSplit(100m, null!));

[Fact]
public void EqualSplit_EmptyParticipants_Throws() =>
Assert.Throws<ArgumentException>(() => SplitCalculator.EqualSplit(100m, Array.Empty<int>()));

[Fact]
public void ByAmount_NullEntries_Throws() =>
Assert.Throws<ArgumentException>(() => SplitCalculator.ByAmount(null!, 100m));

[Fact]
public void ByAmount_EmptyEntries_Throws() =>
Assert.Throws<ArgumentException>(() => SplitCalculator.ByAmount(Array.Empty<(int, decimal)>(), 100m));

[Fact]
public void ByPercentage_NullEntries_Throws() =>
Assert.Throws<ArgumentException>(() => SplitCalculator.ByPercentage(null!, 100m));

[Fact]
public void ByPercentage_ZeroTotal_Throws() =>
Assert.Throws<ArgumentException>(() => SplitCalculator.ByPercentage(new[] { (1, 100m) }, 0m));

[Fact]
public void ByPercentage_AcceptsBoundaryPercentagesZeroAndHundred()
{
var result = SplitCalculator.ByPercentage(new[] { (1, 0m), (2, 100m) }, 100m);

Assert.Equal(0m, result[0].AmountOwed);
Assert.Equal(100m, result[1].AmountOwed);
}

[Fact]
public void ByPercentage_AbsorbsRoundingDriftIntoLastParticipant()
{
var result = SplitCalculator.ByPercentage(
new[] { (1, 33.33m), (2, 33.33m), (3, 33.33m) },
100m);

Assert.Equal(100m, result.Sum(r => r.AmountOwed));
Assert.Equal(33.33m, result[0].AmountOwed);
Assert.Equal(33.34m, result[2].AmountOwed);
}
}
15 changes: 15 additions & 0 deletions stryker-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"stryker-config": {
"project": "SplitIt.API/SplitIt.Infrastructure/SplitIt.Infrastructure.csproj",
"test-projects": ["SplitIt.Tests/SplitIt.Tests.csproj"],
"mutate": ["Services/SplitCalculator.cs"],
"test-case-filter": "FullyQualifiedName~SplitCalculator",
"thresholds": {
"high": 80,
"low": 70,
"break": 65
},
"reporters": ["progress", "html", "json"],
"concurrency": 4
}
}
Loading