Testing & Reliable Design · Lesson 14

Bounded invariant checks

Test useful properties over a declared finite set without claiming a universal proof.

Define the observable contract

An invariant describes a property that should hold across a set of inputs or state transitions. It can complement concrete expected examples when the property follows from the specification. This clamp operation must return a value from zero through five and preserve an already in-range input unchanged. The lesson checks both properties over an explicit bounded set. It is not a full property-based testing library and does not claim to prove the rule for every possible integer. Its strength comes from clearly stated properties and a declared finite domain, not from the mere presence of a loop.

Walk through the real program

The loop visits integers negative two through seven inclusive, producing ten cases. For every result it checks the output interval. For original inputs from zero through five it separately checks identity. The unchanged program prints cases:10, range:True, and identity:True. Those booleans have independent meanings: a constant return value of two would satisfy the range property yet violate identity for most in-range inputs. Keeping both observations visible helps expose a class of wrong implementations that one plausible-looking property would accept.

Why the design is testable

The tested domain should be chosen deliberately. This small range includes values below the minimum, both boundaries, interior values, and values above the maximum. It does not cover integer extremes or every representation concern, so add relevant examples when the implementation introduces arithmetic that could overflow. The expectation should express a specification-level relationship rather than repeat the exact production branches. A real property-testing framework can add generators, shrinking, and reporting, but those features require the genuine tool; they are not supplied automatically by this portable loop.

A boundary that matters

A complicated property can be wrong just like production code. Keep it simple enough to review independently, and combine it with hand-written examples. Reproducing the clamp's full conditional logic in an expected-value calculation would make the check less independent. Also do not let a loop's summary hide which input failed when the tested set grows. A practical checker should preserve the counterexample and the observed result. The current sample keeps the mechanics compact for teaching, while its declared ten-case limit makes the evidence easy to audit by hand.

Practice: isolate one behavior

First replace the implementation with a deliberately constant result of two. Predict range true but identity false. Hint: the properties challenge different mistakes; the range check alone is too weak. Restore the implementation, then remove the lower-bound handling and test the same finite domain again. Record a concrete negative input that violates the interval. Save that input as an ordinary regression example so it remains understandable without rerunning a generated exploration or reading every loop iteration.

Practice: challenge another assumption

For another exercise, add an idempotence property: clamping an already clamped result should leave it unchanged. Hint: justify that relation from the specification instead of assuming every transformation is idempotent. Test the original finite domain and add explicit integer-minimum and integer-maximum examples using a safe implementation. Compare these properties with a small table of exact expected outputs. Explain why several passing properties still do not justify claiming a mathematical proof unless the entire relevant domain and reasoning genuinely support that conclusion.

Review the evidence

A useful property check names its domain, its rule, and the wrong implementations it can distinguish. The baseline's ten cases provide bounded evidence for two complementary invariants. They are stronger than an unexplained collection of random inputs but narrower than a universal proof. When the check fails, preserve the smallest informative counterexample and investigate whether the implementation, property, or stated contract is wrong. That habit connects exploratory testing to durable regression cases while keeping the limits of a lightweight console demonstration honest. For stateful models, inspect rejected commands as well as accepted ones; a rejection must not corrupt the same invariants that successful operations preserve.

  • The finite domain contains exactly ten prepared integer cases.
  • Range and identity catch different classes of incorrect implementation.
  • Bounded property checks are evidence, not an automatic universal proof.

A working example

using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;

int Clamp(int value) => value < 0 ? 0 : value > 5 ? 5 : value;
bool inRange = true, unchangedInside = true;
int cases = 0;
for (int value = -2; value <= 7; value++)
{
    int result = Clamp(value);
    inRange &= result >= 0 && result <= 5;
    if (value >= 0 && value <= 5) unchangedInside &= result == value;
    cases++;
}
Console.WriteLine($"cases:{cases}");
Console.WriteLine($"range:{inRange}");
Console.WriteLine($"identity:{unchangedInside}");

Expected output

cases:10
range:True
identity:True
Try this
State the tested domain and combine properties that catch different wrong implementations.

Further reading