Testing & Reliable Design · Lesson 15

Metamorphic relations for transformations

Relate several runs while retaining an independent concrete example.

Define the observable contract

A metamorphic relation connects the results of several related executions when a useful relationship follows from the requirement. For a sorted unique integer normalization, permuting the input should not change the normalized values, adding an already-present duplicate should not change them, and normalizing an already normalized result should be idempotent. These relations are helpful when many exact outputs would be tedious to enumerate. They still need independent justification and concrete examples, because an incorrect constant function can satisfy several attractive relations without implementing normalization at all.

Walk through the real program

The input contains three, one, three, and two. Normalize removes duplicates and sorts ascending, producing one, two, and three. Reversing the input yields the same normalized array, so permutation prints true. Appending another three leaves the distinct result unchanged, so duplicate prints true. Normalizing the result again also matches, and the independent literal example array matches as well. The four original lines are therefore permutation:True, duplicate:True, idempotent:True, and example:True. No randomness, timing, or external service participates in those observations.

Why the design is testable

The final literal comparison is important. Relations can expose inconsistent behavior across transformations, while an example anchors the intended result. A function always returning an empty array would pass the first three relations but fail the example. Other incorrect functions can pass one relation and fail another. Think about such countermodels before deciding a property is useful. The normalization implementation uses real LINQ operators over local integers; the checks are ordinary console comparisons rather than a library providing automated relation generation, shrinking, or a complete testing strategy.

A boundary that matters

A relation is valid only for the stated contract. Permutation invariance would be wrong for a function that intentionally preserves first-occurrence order. Duplicate invariance would be wrong for a frequency counter. Sorting with a comparer whose equality differs from the intended identity can create additional subtleties for text or mutable objects. Do not transfer properties between superficially similar APIs without rereading their requirements. Also preserve the input's ownership contract separately: equal normalized outputs do not by themselves prove that the implementation avoided mutating a caller's array.

Practice: isolate one behavior

First replace Normalize with a function returning an empty array and predict three true relations but a false example check. Hint: this demonstrates why relations alone can share a blind spot. Restore the implementation, then remove Distinct and identify which relation now detects the added duplicate. Keep the literal expected result beside the relation checks. Finally try an already sorted unique input and explain why a convenient baseline still needs transformed cases to expose errors the original sequence cannot reveal.

Practice: challenge another assumption

For a second exercise, change the desired contract to stable first-occurrence deduplication without sorting. Decide which current relations remain valid and which must be removed or revised. Hint: reversing the input can legitimately change output order under that new rule. Write an independent example for the revised API and test non-mutation separately. This exercise is about choosing properties from requirements, not accumulating every relation that happened to pass the previous implementation. Document the reason for each accepted relation so future changes can be reviewed coherently.

Review the evidence

The unchanged program combines three justified relations with one concrete expected output. That combination makes its evidence more informative than either unexplained properties or a single convenient example alone. A good metamorphic check remains tied to the domain's ordering, identity, and ownership rules. When a relation fails, investigate whether the implementation violated the contract or the relation assumed a different contract. Keep the tool boundary honest: these portable comparisons demonstrate the reasoning, while a full framework supplies additional execution and diagnostic features when the project needs them. A transformation that sorts but forgets to deduplicate demonstrates why duplicate invariance targets a different defect from permutation or idempotence.

  • The original normalized value is the independent literal sequence one, two, three.
  • A constant wrong result can satisfy several relations without being correct.
  • Choose relations from the actual ordering and identity contract.

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[] Normalize(IEnumerable<int> values) => values.Distinct().OrderBy(value => value).ToArray();
int[] input = { 3, 1, 3, 2 };
int[] normalized = Normalize(input);
Console.WriteLine($"permutation:{normalized.SequenceEqual(Normalize(input.Reverse()))}");
Console.WriteLine($"duplicate:{normalized.SequenceEqual(Normalize(input.Append(3)))}");
Console.WriteLine($"idempotent:{normalized.SequenceEqual(Normalize(normalized))}");
Console.WriteLine($"example:{normalized.SequenceEqual(new[] { 1, 2, 3 })}");

Expected output

permutation:True
duplicate:True
idempotent:True
example:True
Try this
Relations complement expected examples; a constant wrong function can satisfy weak relations.

Further reading