Axis 04 · Conservation: Did anything appear or vanish?
Moving money between your checking and savings accounts. The total is the same before and after. Nothing was created, nothing was destroyed. A quantity that must be conserved is one that operations move around but never change.
A transfer took a one-unit fee. The two accounts together held less after the transfer than before. Totals drift, a little at a time, and nobody notices until the books do not balance.
public static readonly Constraint<Ledger> MoneyIsConserved = Constraints<Ledger>.Conserves(l => l.Total);
It runs the operations from many starting values and compares the quantity you named (here, the total) before and after each. It checks that some operation changed the value at all.
Conservation failed.
Rule broken money-is-conserved did not conserve its value (was 16, became 15). Operation MoveWithFee(2) Started from Ledger { Checking = 10, Savings = 6, Total = 16 } Produced Ledger { Checking = 7, Savings = 8, Total = 15 }
Law No operation creates or destroys a quantity that must be conserved. Consequence Totals drift: something appears or vanishes between the two halves of a change.
Smallest case found, after 2 shrinks. To run this case again: .Seed("db7hMFd37Kd8")
How to read this:
- Rule broken says what was conserved and what happened: it was 39 and became 38.
- Operation is the transfer, with its amount. Started from and Produced show where the unit went.
Conserves fails any operation that changes the quantity it names. A Wallet with Move and Deposit cannot carry Conserves(s => s.Checking + s.Savings) over both: Deposit is meant to change the total, so the rule reports it.
There are two ways to say what you mean, and both pass on a correct Wallet and still catch a leaking Move.
Register only the operations that must keep the total. The rule is then about transfers, and Deposit is tested by other rules (it never lets Checking go negative, for example):
await AutoTest.For(rules, () => new Wallet())
.MutateAsync<int>("Move", (w, n) => w.MoveAsync(n), _ => Gen.Int[1, 12])
.RunAsync();
Or put the inflow in the state, so the conserved quantity includes it. Here Deposited is a running total of money that came in, and the rule reads "what is held, less what came in, never changes":
public sealed record WalletState(int Checking, int Savings, int Deposited);
Constraints<WalletState>.Conserves(s => s.Checking + s.Savings - s.Deposited)
A scenario test checks the amounts it moved. It rarely checks that the sum of everything is unchanged across every operation.
In docs-builder, a deploy planner counted skipped files in its total, so the total no longer equalled additions plus updates plus deletions.
Previous: 03 Monotonicity · Next: 05 Absence · All nine axes