Testing & Reliable Design · Lesson 11

Tests focused on behavior

Preserve the public result and source ownership without locking down private helper calls.

Define the observable contract

A resilient check targets behavior callers are entitled to depend on. Build accepts an integer array, selects nonnegative entries, and returns their count and sum without mutating the supplied array. Those are observable promises. The number of private helper calls or whether the implementation uses one loop or two LINQ operations is not part of this small public contract. Locking tests to those details would make harmless refactoring look like a failure while doing little to protect the real result. This lesson therefore checks values and source preservation directly, with no interaction fake needed.

Walk through the real program

The prepared array contains three, negative one, and five. Two entries qualify, and their sum is eight. The first output is count:2;total:8. Before calling Build, the program copied the input array's integer values. Comparing the current input with that independent copy prints source-preserved:True. These observations protect both the calculation and this function's no-mutation contract. The copy is shallow, which is sufficient for integers; the same technique would need more careful ownership reasoning for an array containing mutable objects whose internal fields could change.

Why the design is testable

A behavior-focused test should survive an implementation change that preserves its contract. Replacing the Count and Where/Sum pipeline with a straightforward loop can be correct even though internal enumeration counts differ. That does not mean all interaction checks are bad. A notification boundary may explicitly require one externally visible effect, as the previous lesson showed. The distinction is whether the interaction is promised to the consumer. Keep tests descriptive enough that a reviewer can say why each observation matters rather than pointing only to what a mocking library happens to make easy to verify.

A boundary that matters

A test that compares only the final total would miss a wrong count, while a count alone would miss a wrong sum. A source-preservation check adds a different dimension. Conversely, checking every private method's call order can overspecify an algorithm without adding relevant coverage. Think about plausible wrong implementations and which independent observation exposes each one. A filter that includes the negative value changes the total; a method that sorts its input in place might preserve some numerical results but violate the original ownership promise. Use cases chosen to distinguish those behaviors.

Practice: isolate one behavior

First add zero to the input and predict three accepted entries with the same total eight. Hint: nonnegative includes zero, so count and total respond differently. Then add another negative value and predict no change to the summary. Check the copied input afterward in both cases. These variations verify the exact selection rule rather than assuming every nonpositive number is excluded. Keep a deliberately unsorted input when experimenting with an implementation that might mutate order, so the source-preservation observation has something meaningful to detect.

Practice: challenge another assumption

For a second exercise, implement Build with a single explicit loop and run the same result and preservation checks without adding assertions about loop iterations. Hint: preserve the public Summary contract while letting private organization vary. Then decide whether a new requirement to return the accepted values changes what consumers can observe. Add only the ownership and ordering checks that new result actually promises. Do not infer deep immutability from a read-only-looking property if the returned model exposes nested mutable storage.

Review the evidence

The original two lines prove specific behavior with one informative input. They are neither a complete correctness proof nor evidence of the most efficient implementation. A useful suite combines relevant examples, boundary cases, and ownership checks while allowing legitimate internal refactoring. This is one practical way testing can improve design: the public contract becomes clearer, and implementation details remain free to change where callers should not care. Overmocking often obscures that distinction by making the test repeat the current implementation's choreography rather than describe its obligations.

  • The baseline accepts two entries totaling eight and preserves the input.
  • Externally promised effects differ from incidental private helper calls.
  • A harmless one-loop refactor should satisfy the same behavior checks.

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;

Summary Build(int[] source) => new(source.Count(value => value >= 0), source.Where(value => value >= 0).Sum());
int[] input = { 3, -1, 5 };
int[] before = input.ToArray();
var result = Build(input);
Console.WriteLine($"count:{result.Count};total:{result.Total}");
Console.WriteLine($"source-preserved:{input.SequenceEqual(before)}");
record Summary(int Count, int Total);

Expected output

count:2;total:8
source-preserved:True
Try this
Observe the contract so a harmless internal refactor does not break the check.

Further reading