From fb3a593e90053bc1480100ff0da1f193d4c321ce Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Wed, 30 Sep 2026 15:05:13 +0100 Subject: [PATCH] Add Has.OrderState custom matcher, refactor DiscountOnFirstLine_TotalAndLineValues as example --- .../tests/OrderTests/OrderStateConstraint.cs | 74 +++++++++++++++++++ csharp/tests/OrderTests/OrderTotalTests.cs | 10 ++- 2 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 csharp/tests/OrderTests/OrderStateConstraint.cs diff --git a/csharp/tests/OrderTests/OrderStateConstraint.cs b/csharp/tests/OrderTests/OrderStateConstraint.cs new file mode 100644 index 0000000..430ed5f --- /dev/null +++ b/csharp/tests/OrderTests/OrderStateConstraint.cs @@ -0,0 +1,74 @@ +using System.Linq; +using NUnit.Framework.Constraints; +using OrderDomain; + +namespace OrderTests; + +/// +/// Custom NUnit matcher that bundles multiple order assertions into one. +/// Participants: you do not need to understand how this works — +/// just recognize the pattern and use it as an example for the agent. +/// +public static class Has +{ + public static IResolveConstraint OrderState(decimal expectedTotal, decimal[] expectedLineValues) + => new OrderStateConstraint(expectedTotal, expectedLineValues); +} + +public class OrderStateConstraint : Constraint +{ + private readonly decimal _expectedTotal; + private readonly decimal[] _expectedLineValues; + + public OrderStateConstraint(decimal expectedTotal, decimal[] expectedLineValues) + { + _expectedTotal = expectedTotal; + _expectedLineValues = expectedLineValues; + } + + public override string Description => + $"order with total {_expectedTotal} and line values [{string.Join(", ", _expectedLineValues)}]"; + + public override ConstraintResult ApplyTo(TActual actual) + { + var order = (Order)(object)actual!; + var actualTotal = order.Total; + var actualLineValues = order.Lines.Select(l => l.Value).ToArray(); + + var details = new List(); + if (actualTotal != _expectedTotal) + details.Add($"Total: expected {_expectedTotal}, but was {actualTotal}"); + if (!actualLineValues.SequenceEqual(_expectedLineValues)) + details.Add( + $"Line values: expected [{string.Join(", ", _expectedLineValues)}], " + + $"but was [{string.Join(", ", actualLineValues)}]"); + + return new OrderStateResult(this, actual!, details.Count == 0, details); + } + + /// + /// A ConstraintResult that carries the per-field mismatch details and + /// writes them as additional lines in the failure message, e.g. + /// + /// Expected: order with total 210 and line values [160, 50] + /// But was: <Order> + /// Total: expected 210, but was 250 + /// Line values: expected [160, 50], but was [200, 50] + /// + private sealed class OrderStateResult : ConstraintResult + { + private readonly IReadOnlyList _details; + + public OrderStateResult(Constraint constraint, object? actual, bool isSuccess, IReadOnlyList details) + : base(constraint, actual, isSuccess) + { + _details = details; + } + + public override void WriteAdditionalLinesTo(MessageWriter writer) + { + foreach (var detail in _details) + writer.WriteMessageLine(detail); + } + } +} diff --git a/csharp/tests/OrderTests/OrderTotalTests.cs b/csharp/tests/OrderTests/OrderTotalTests.cs index 682a2b8..94608e5 100644 --- a/csharp/tests/OrderTests/OrderTotalTests.cs +++ b/csharp/tests/OrderTests/OrderTotalTests.cs @@ -5,6 +5,8 @@ namespace OrderTests; public class OrderTotalTests { + // This test was refactored — notice the custom matcher. + // The other tests still need the same treatment. [Test] public void DiscountOnFirstLine_TotalAndLineValues() { @@ -14,9 +16,9 @@ public class OrderTotalTests order.ApplyDiscount(0); - Assert.That(order.Total, Is.EqualTo(210m)); - Assert.That(order.Lines[0].Value, Is.EqualTo(160m)); - Assert.That(order.Lines[1].Value, Is.EqualTo(50m)); + Assert.That(order, Has.OrderState( + expectedTotal: 210m, + expectedLineValues: [160m, 50m])); } [Test] @@ -87,4 +89,4 @@ public class OrderTotalTests Assert.That(order.Lines[1].Value, Is.EqualTo(48m)); Assert.That(order.Lines[2].Value, Is.EqualTo(30m)); } -} +} \ No newline at end of file