Testing & Reliable Design · Lesson 17

Pure rules and parsing adapters

Separate representation failures from domain rejections.

Define the observable contract

A boundary adapter often receives text while the domain rule needs a typed value. Treat conversion and business acceptance as distinct stages with distinct failures. Evaluate uses an explicitly narrow invariant integer grammar, then asks whether the quantity can be reserved against five available units. A malformed string and a well-formed but rejected quantity should not become the same unexplained zero. Keeping these stages separate lets tests challenge the parser contract, the pure domain predicate, and their composition without opening a file or presenting a user interface in every small scenario.

Walk through the real program

The text three parses successfully and passes the positive-within-capacity rule, so the first output is accepted:3. The letter x cannot be parsed under the selected grammar and produces invalid:text. Zero is a valid integer representation but violates the positive-quantity rule, producing invalid:quantity. The distinction is visible in three deterministic lines. NumberStyles.None intentionally does not permit signs or surrounding whitespace; those are separate syntax choices, not automatically accepted conveniences. The example makes no claim that the same grammar is appropriate for every human-facing numeric field.

Why the design is testable

Test the domain predicate directly with typed values so parsing failures cannot mask an incorrect business rule. Test the adapter with representative text to confirm how successful and unsuccessful conversions are mapped. Finally, check the composed behavior for a few important paths. This layering avoids a common testing problem: every scenario passes through many unrelated components, so an early rejection prevents the test from exercising the rule its name claims to cover. A good boundary case arranges all other prerequisites valid when it intends to isolate one specific rejection.

A boundary that matters

With the current flags, text negative one is rejected as a representation, so it would not test the domain's negative-quantity branch through this adapter. Use a direct typed predicate case for that domain rule, or deliberately widen the grammar and update the contract. Also avoid silently trimming or replacing text unless that normalization is specified. Catching all exceptions and returning invalid:text would conceal unexpected defects unrelated to conversion. TryParse already provides an ordinary failure result for this anticipated syntax boundary; preserve the distinction instead of hiding other errors behind it.

Practice: isolate one behavior

First test the text six and predict invalid:quantity, because conversion succeeds but the quantity exceeds capacity five. Then test a space-padded three and predict invalid:text under NumberStyles.None. Hint: identify the stage that rejects each input before running. Add a direct CanReserve call with negative one to cover the typed domain behavior independently. These cases demonstrate why a table of invalid strings alone cannot establish that every rejected business quantity was actually evaluated by the domain predicate.

Practice: challenge another assumption

For a second exercise, decide that surrounding whitespace is allowed at ingestion and add only the corresponding parsing policy or explicit normalization. Update the padded-input expectation while keeping zero and excess-capacity behavior unchanged. Hint: a syntax change does not justify altering the pure rule. Return a structured result with a named error code instead of the current display strings, then put human formatting in another function. Test the stable result code separately from presentation so a wording change does not obscure a broken acceptance decision.

Review the evidence

The unchanged program gives one accepted path and two visibly different rejections. Its value lies in the clear boundaries, not in the number of labels printed. Reliable application design makes the parser's grammar, domain rule, and result mapping independently reviewable, while appropriate integration checks verify they connect correctly. When an intended branch is difficult to reach, inspect whether another prerequisite rejects the case first. That habit prevents confounded tests and makes failure evidence precise enough to guide a meaningful fix. A typed quantity outside the parser grammar still belongs in direct domain tests when that domain API accepts the value as an argument.

  • Three is accepted, x fails parsing, and zero fails the domain rule.
  • The current narrow grammar rejects signed and padded representations.
  • Keep every other prerequisite valid when isolating a specific rejection.

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;

string Evaluate(string text)
{
    if (!int.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out int quantity)) return "invalid:text";
    if (!CanReserve(quantity, 5)) return "invalid:quantity";
    return "accepted:" + quantity;
}
bool CanReserve(int requested, int available) => requested > 0 && requested <= available;
Console.WriteLine(Evaluate("3"));
Console.WriteLine(Evaluate("x"));
Console.WriteLine(Evaluate("0"));

Expected output

accepted:3
invalid:text
invalid:quantity
Try this
Test the parser grammar and business predicate independently before checking their adapter.

Further reading