Type-Safe Table-Driven Test Cases

Define named, typed test cases with duplicate-name checks and run them against an executor, returning pass counts and failures.

TypescriptData Integrity

Code

snippet
export interface TestCase<Input, Expected> {
  name: string;
  input: Input;
  expected: Expected;
}

export interface CaseFailure<Expected> {
  name: string;
  actual: Expected;
  expected: Expected;
}

export interface CaseRunResult<Expected> {
  passed: number;
  failures: Array<CaseFailure<Expected>>;
}

export function defineCases<Input, Expected>(
  cases: ReadonlyArray<TestCase<Input, Expected>>,
): ReadonlyArray<TestCase<Input, Expected>> {
  if (!Array.isArray(cases) || cases.length === 0) {
    throw new RangeError("cases must be a non-empty array");
  }
  const names = new Set<string>();
  for (const testCase of cases) {
    if (typeof testCase.name !== "string" || testCase.name.length === 0) {
      throw new TypeError("each case needs a non-empty name");
    }
    if (names.has(testCase.name)) {
      throw new RangeError("duplicate case name: " + testCase.name);
    }
    names.add(testCase.name);
  }
  return cases;
}

export function runCases<Input, Expected>(
  cases: ReadonlyArray<TestCase<Input, Expected>>,
  execute: (input: Input) => Expected,
  equals: (actual: Expected, expected: Expected) => boolean = Object.is,
): CaseRunResult<Expected> {
  if (typeof execute !== "function") throw new TypeError("execute must be a function");
  if (typeof equals !== "function") throw new TypeError("equals must be a function");
  let passed = 0;
  const failures: Array<CaseFailure<Expected>> = [];
  for (const testCase of cases) {
    const actual = execute(testCase.input);
    if (equals(actual, testCase.expected)) {
      passed += 1;
    } else {
      failures.push({
        name: testCase.name,
        actual,
        expected: testCase.expected,
      });
    }
  }
  return { passed, failures };
}