Axis 01 · Invariant: Is it true in every state?

A rule that must be true every single time you look at the thing. An order total is never negative. A confirmed order has lines. It does not matter how the order got into its state: if you look, the rule holds.

A method added a line with a negative price and the total dropped below zero. Every order the tests had made was valid, so a test that only looked at made-up orders never saw it. The bug was not in a value. It was in an operation.

public static readonly Constraint<Order> TotalNeverNegative = Constraints<Order>.Always(o => o.Total >= 0, "An order total is never negative.");

public static readonly Constraint<Order> ConfirmedOrdersHaveLines = Constraints<Order>
	.When(o => o.Status == OrderStatus.Confirmed)
	.Requires(o => o.Lines.Count > 0, "A confirmed order has at least one line.");

public static readonly Constraint<Order> LegalMoves = Constraints<Order>
	.Transitions(o => o.Status)
	.Allow(OrderStatus.Pending, OrderStatus.Confirmed)
	.Allow(OrderStatus.Confirmed, OrderStatus.Shipped);
		

Also Each, Unique, References, OneOf and the others in the vocabulary.

It makes many valid orders and checks every rule on each. Then it runs every operation of the type (every public method that returns an Order) starting from valid orders, and checks the rules again on what comes out. A rule that is only a warning is allowed through, as your own operations allow it.

Invariant failed.

Rule broken total-never-negative: An order total is never negative. Operation AddRawLine() Started from Order { Lines = [], Status = Pending, Version = 0, Total = 0, Label = "" } Produced Order { Lines = [OrderLine { Sku = "fig", Quantity = 1, Price = -5 }], Status = Pending, Version = 0, Total = -5, Label = "fig" }

Law Every generated value satisfies every rule that must hold in every state, and each of those rules can fire. Consequence A value that breaks a stated rule is accepted into the domain, and nothing shows the rule can even be broken.

Smallest case found, after 1 shrink. To run this case again: .Seed("9Rmf9J10O1j6")

How to read this:

  • Rule broken names the rule that did not hold, and says what it required.
  • Operation is the call that did it. AddRawLine() took no argument.
  • Started from is a valid order. Produced is what the operation gave back, and it is not valid.

A scenario test builds the orders its author imagined. A rule broken by a method nobody thought to call with a bad input stays invisible until production.

In docs-builder, running the rules over generated data found two rules that crashed on hostile values (a null default call-to-action; a path prefix that is not a legal URL), before any user did.


Next: 02 Idempotency · All nine axes