Add Has.OrderState custom matcher, refactor DiscountOnFirstLine_TotalAndLineValues as example

This commit is contained in:
2026-10-01 11:38:06 +01:00
parent 98f649f46b
commit fb3a593e90
2 changed files with 80 additions and 4 deletions
@@ -0,0 +1,74 @@
using System.Linq;
using NUnit.Framework.Constraints;
using OrderDomain;
namespace OrderTests;
/// <summary>
/// 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.
/// </summary>
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>(TActual actual)
{
var order = (Order)(object)actual!;
var actualTotal = order.Total;
var actualLineValues = order.Lines.Select(l => l.Value).ToArray();
var details = new List<string>();
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);
}
/// <summary>
/// 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: &lt;Order&gt;
/// Total: expected 210, but was 250
/// Line values: expected [160, 50], but was [200, 50]
/// </summary>
private sealed class OrderStateResult : ConstraintResult
{
private readonly IReadOnlyList<string> _details;
public OrderStateResult(Constraint constraint, object? actual, bool isSuccess, IReadOnlyList<string> details)
: base(constraint, actual, isSuccess)
{
_details = details;
}
public override void WriteAdditionalLinesTo(MessageWriter writer)
{
foreach (var detail in _details)
writer.WriteMessageLine(detail);
}
}
}
+6 -4
View File
@@ -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));
}
}
}