Invariants & Mutation Checks

The Course
•
4 min read

An invariant holds across a family of valid inputs. For a pure mapping operation, output length equals input length, each result corresponds to its original position, and input values remain unchanged. The pool adds a peak-active bound and a no-new-starts-after-failure rule.

You can exercise small deterministic input families without introducing a testing library. Test lengths zero through several values and more than one limit. Keep schedules controlled separately; a loop over lengths alone does not test out-of-order completion.

Mutation checking asks whether existing expectations distinguish correct code from a plausible broken version. If changing indexed assignment to push still passes every test, the order contract is untested. If removing cleanup still passes, your lifecycle claim lacks evidence.

Do not write dozens of assertions that simply repeat implementation steps. Test outcomes and relationships a caller depends on. A passing mutation experiment supports those particular claims; it does not prove every possible defect is detected.

Predict, run, and explain

Write the output and trace the important state changes before checking it. Then run the program. Each editor is independent. Async examples end with main() so this editor waits for their work.

Predict, then run JavaScript
function assert(condition, message) {
  if (!condition) {
    throw new Error(message);
  }
}

for (let length = 0; length <= 4; length++) {
  const values = Array.from({ length }, (_, index) => index);
  const doubled = values.map(value => value * 2);

  assert(doubled.length === values.length, "length");
  assert(
    doubled.every((value, index) => value === index * 2),
    "positions"
  );
}

console.log("5 input families passed");
Check your prediction
5 input families passed

Test a family of inputs with a shared rule

An invariant describes behavior across multiple cases. For a pure reverse, output length is preserved, reversing twice restores the values, and the input stays unchanged. These properties catch different defects; no one property proves the entire algorithm correct.

Use small generated families you can inspect without a property-testing framework. Include empty and repeated-value cases, and still keep a few explicit expected outputs. A wrong implementation can satisfy one algebraic property by accident, such as returning its input unchanged.

Test a family of inputs with a shared rule: trace JavaScript
function reverse(values) {
  return [...values].reverse();
}

for (const values of [[], [1], [1, 2], [1, 1, 2]]) {
  const twice = reverse(reverse(values));

  if (JSON.stringify(twice) !== JSON.stringify(values)) {
    throw new Error("Round trip failed");
  }
}

console.log("4 round trips passed");
Check your prediction
4 round trips passed

The round-trip property covers a family, but an identity function also passes it. Combine it with a concrete directional expectation and ownership checks.

Test a family of inputs with a shared rule: contrast JavaScript
const before = [1, 2, 3];
const after = [...before].reverse();

if (after.join(",") !== "3,2,1" || before.join(",") !== "1,2,3") {
  throw new Error("Contract failed");
}

console.log("order and nonmutation passed");
Check your prediction
order and nonmutation passed

The explicit output distinguishes reversal from doing nothing. The second check distinguishes a copying reverse from mutating the caller’s array.

Try the rule on new inputs

For a pool, propose invariants for result length, input order and active-call bounds. Explain which need controlled schedules as well as small input families. Mutate one rule and identify the distinguishing test.

Your turn

Exercise

Invariants & Mutation Checks: practise

Test a function that returns a reversed copy for lengths 0 through 4. Assert equal length, unchanged input and correct reversed values. Print 5 families passed only after all checks.

Your workspace JavaScript
// Use JSON.stringify for the unchanged-input comparison in this numeric-only example.
Hint 1: the idea

A correct copy has the reversed values without changing the source.

Hint 2: a first step

Save the input representation before calling the helper.

Show one solution
function reversed(values) {
  return [...values].reverse();
}

function assert(condition) {
  if (!condition) {
    throw new Error("Contract failed");
  }
}

for (let length = 0; length <= 4; length++) {
  const input = Array.from({ length }, (_, index) => index);
  const before = JSON.stringify(input);
  const output = reversed(input);

  assert(output.length === length);
  assert(JSON.stringify(input) === before);
  assert(output.every((value, index) => value === length - 1 - index));
}

console.log("5 families passed");
5 families passed

Explain why the implementation meets the stated contract. Which part is enforced by the language or checker, and which part needs behavioral evidence?

Change it and test it

Mutate reversed to call reverse directly on its input. Confirm your nonmutation expectation fails. Explain why length zero and one alone would miss this mutation. For the task pool, add a schedule case rather than assuming more input sizes test concurrency.

Was this article helpful?

Your response is saved on this device.