Skip to main content
Version: v0.1.11

Property-test your own code with generated interchanges

A test written by hand checks the interchanges its author thought of. A property-based test states a rule, such as "this never throws" or "what I write reads back", and checks it on as many generated inputs as you ask for. @cosyte/x12/arbitraries exports the generators we use to test this library, built on fast-check, so you can point them at code you build on it.

Every generated value is synthetic and random: nothing in one looks like a patient's name, date of birth, member id or address, or an amount from a real claim, so a failing case is safe to paste into an issue.

Install fast-check​

npm install --save-dev fast-check

fast-check is an optional peer dependency of @cosyte/x12. Only @cosyte/x12/arbitraries loads it; the main entry point never does, so code that only parses and builds interchanges does not need it.

Check that your code handles any interchange​

interchange() generates a parsed X12Interchange: an ISA over generated delimiters, sometimes a TA1, then one or more functional groups of one or more transaction sets. Each transaction set is a type the library declares a guide for (270, 271, 276, 277, 278, 820, 834, 835 or 837), and its body segments carry repetitions, components and values that need the release character. Every count and control number agrees, so the parser warns about nothing. State the rule your code must keep, and let fast-check look for an interchange that breaks it.

import fc from "fast-check";
import type { X12Interchange } from "@cosyte/x12";
import { interchange } from "@cosyte/x12/arbitraries";

const QUEUES = ["remittance", "claims", "eligibility", "other"];

// Your code: pick a queue for each transaction set by its ST-01.
function queuesFor(ix: X12Interchange): string[] {
return ix.groups.flatMap((group) =>
group.transactions.map((tx) => {
switch (tx.st.elements[1]) {
case "835":
return "remittance";
case "837":
return "claims";
case "270":
case "271":
return "eligibility";
default:
return "other";
}
}),
);
}

// The rule: every transaction set lands in a known queue, and none is skipped.
fc.assert(
fc.property(interchange(), (ix) => {
const queues = queuesFor(ix);
const count = ix.groups.reduce((n, group) => n + group.transactions.length, 0);
return queues.length === count && queues.every((q) => QUEUES.includes(q));
}),
{ numRuns: 50 },
);

const [sample] = fc.sample(interchange(), { numRuns: 1, seed: 1 });
sample?.warnings.length; // => 0

Check that what you write reads back​

interchangeSpec() generates what buildInterchange takes, and value() generates the text of one element as a reader sees it, including the delimiters and the ? that have to be released on the wire. Pair them, and the rule is the one that matters most for code that writes X12: what you put in comes back out.

import fc from "fast-check";
import {
buildInterchange,
getSegmentValue,
parseX12,
serializeX12,
type InterchangeSpec,
} from "@cosyte/x12";
import { interchangeSpec, value } from "@cosyte/x12/arbitraries";

// Your code: add a note to the first transaction set before sending it on.
function withNote(spec: InterchangeSpec, note: string): InterchangeSpec {
const [group, ...groups] = spec.groups;
const [tx, ...txs] = group?.transactions ?? [];
if (group === undefined || tx === undefined) return spec;
const noted = { ...tx, segments: [...tx.segments, ["NTE", "ADD", note]] };
return { ...spec, groups: [{ ...group, transactions: [noted, ...txs] }, ...groups] };
}

fc.assert(
fc.property(interchangeSpec(), value(), (spec, note) => {
const ix = parseX12(serializeX12(buildInterchange(withNote(spec, note))));
const segments = ix.groups[0]?.transactions[0]?.segments ?? [];
const nte = segments[segments.length - 2]; // the SE is last
return nte !== undefined && getSegmentValue(nte, "02", ix.delimiters) === note;
}),
{ numRuns: 50 },
);

const spec: InterchangeSpec = {
senderId: "SENDER",
receiverId: "RECEIVER",
interchangeDate: "260101",
interchangeTime: "1200",
interchangeControlNumber: "1",
groups: [
{
functionalIdCode: "HC",
groupControlNumber: "1",
versionRelease: "005010X222A2",
transactions: [
{ transactionSetIdCode: "837", transactionSetControlNumber: "0001", segments: [] },
],
},
],
};
serializeX12(buildInterchange(withNote(spec, "a*b"))).includes("~NTE*ADD*a?*b~"); // => true

Feed your code messy and hostile input​

Two generators produce raw input rather than parsed interchanges:

  • interchangeText() is text a real sender might produce: line breaks between segments, an older version in the ISA, counts and control numbers that disagree, a missing SE, GE or IEA, a TA1 inside a group, segments outside any transaction set, bytes after the IEA, no final terminator, trailing empty elements, a lower-case segment id, characters outside the X12 character sets and a binary segment. The ISA is always whole, so the parser accepts all of it, with a warning for what it departs from.
  • hostileInput() is random strings, random bytes, interchanges cut short or with text inserted and deleted, binary segments with a wrong octet count, and interchanges as bytes. The parser may refuse these, with one of its four documented fatal errors, but throws nothing else.
import fc from "fast-check";
import { X12ParseError, parseX12 } from "@cosyte/x12";
import { hostileInput, interchangeText } from "@cosyte/x12/arbitraries";

// Your code: read the interchange control number off a file drop without crashing the job.
function readControlNumber(raw: string | Buffer): string | undefined {
try {
return parseX12(raw).isa.elements[13];
} catch (err) {
if (err instanceof X12ParseError) return undefined;
throw err;
}
}

// Messy text always parses, so its control number is always there.
fc.assert(
fc.property(interchangeText(), (raw) => readControlNumber(raw) !== undefined),
{ numRuns: 50 },
);

// Hostile input may be refused, but never crashes the reader.
fc.assert(
fc.property(hostileInput(), (raw) => {
readControlNumber(raw);
}),
{ numRuns: 50 },
);

readControlNumber("not an interchange"); // => undefined

Pick the generators​

GeneratorGeneratesOptions
interchange()a parsed, spec-clean X12Interchange that serializes back to the text it was parsed fromdelimiters, maxGroups, maxTransactions, maxSegments, maxElements
interchangeText()messy interchange text the parser acceptsthe same as interchange()
hostileInput()a string or a Buffer the parser may refusemaxLength
interchangeSpec()an InterchangeSpec that buildInterchange builds with no warningthe same as interchange()
transactionSetType(){ id, functionalIdCode, version }, such as { id: "835", functionalIdCode: "HP", version: "005010X221A1" }none
segment()a body segment as buildInterchange takes one: the id, then each element's valueids, maxElements, values
value()the text of one element or component, from the X12 character sets, delimiters and ? includedminLength, maxLength
delimiters()a delimiter set: the standard *, ^, : and ~, or any four distinct punctuation characters but ?delimiters

segment() and the elements interchange() writes end on a non-empty element, because X12 writes no trailing empty element, so a generated interchange reads back exactly as it was generated.

Replay a failure​

When a rule fails, fast-check shrinks the case to a small one and reports the seed and path that produced it. A failing interchange prints as the parseX12(...) call that rebuilds it.

import fc from "fast-check";
import { interchange } from "@cosyte/x12/arbitraries";

// A rule that is wrong: not every interchange carries claims.
const everyInterchangeHasClaims = fc.property(interchange(), (ix) =>
ix.groups.some((group) => group.gs.elements[1] === "HC"),
);

let report = "";
try {
fc.assert(everyInterchangeHasClaims, { seed: 42 });
} catch (err) {
report = String(err);
}
report.includes("seed: 42"); // => true
report.includes('Counterexample: [parseX12("ISA'); // => true

Pass the reported seed and path back to fc.assert to run that one case again, already shrunk, while you fix it. A test in CI can run more cases than one on a laptop: set numRuns from an environment variable, as we do with COSYTE_PROPERTY_RUNS.

Reference​