What a parameterized test does
A parameterized test separates the behavior you want to check from the examples used to check it. Instead of writing nearly identical tests for five inputs, you put each input and expected result in a table and run the same test logic for every row. This is useful when a function has a simple contract but several important cases, such as values on either side of a threshold.
The table does not replace thinking about behavior. Each row should have a reason to exist. Several values far from a threshold may tell you less than one value below it, one at it, and one above it. Parameterized tests work best when every row can use the same setup and assertion; if a case needs very different preparation, a separate test may be clearer.
Build a small, runnable example
This example uses ES modules and the built-in test runner in Node.js 20 or later. Create two files in the same directory. The contract for classifyScore is deliberately narrow: it accepts an integer from 0 through 100 and returns "pass" for 70 or higher, otherwise "retry". Input validation is outside this example’s contract.
In score.mjs, write: export function classifyScore(score) { return score >= 70 ? 'pass' : 'retry'; }
In score.test.mjs, write: import test from 'node:test'; import assert from 'node:assert/strict'; import { classifyScore } from './score.mjs'; const cases = [ { score: 0, expected: 'retry' }, { score: 69, expected: 'retry' }, { score: 70, expected: 'pass' }, { score: 71, expected: 'pass' }, { score: 100, expected: 'pass' }, ]; for (const { score, expected } of cases) { test(`classifyScore(${score}) returns ${expected}`, () => { assert.equal(classifyScore(score), expected); }); }
Run node --test score.test.mjs from that directory. The loop registers one test per row, so a failure identifies the score involved. The assertion stays in one place, while the table makes the expected behavior visible at a glance.
Choose cases that expose mistakes
Start with the contract, not with a long list of convenient numbers. For a cutoff at 70, scores 69 and 70 distinguish >= from >. Score 71 shows that values above the cutoff still pass. Scores 0 and 100 cover the permitted endpoints. If the function later gains another branch, add cases around that branch rather than simply adding more arbitrary values.
A descriptive name helps when a case fails, but the expected result should also remain explicit in the table. Avoid calculating it with logic that mirrors the implementation, such as setting expected to score >= 70 ? 'pass' : 'retry'. If the threshold or comparison is wrong in both places, the test can agree with the bug. Record the result the contract requires for each selected input instead.
Keep rows independent. A test that changes shared state can make later rows pass or fail depending on order. For a stateful component, create fresh state for each case inside its test callback. If setup or expected outcomes vary substantially between groups, use separate tables or individual tests so the shared assertion remains understandable.
Know when to extend the pattern
This test covers only inputs promised by the contract. Passing a string, null, or an out-of-range number would exercise JavaScript coercion or unspecified behavior, not necessarily reveal a defect in classifyScore. If invalid input must be rejected, define how rejection works first, then add cases that assert that behavior. For example, you might require a RangeError for numbers outside 0 through 100, but the function would need validation before those tests could pass.
Parameterized tests are also useful for string transformations, date formatting with a fixed environment, and interface helpers that map component states to labels. Specify relevant conditions alongside the cases: locale, time zone, DOM environment, or mocked dependencies can affect results. The goal is not to fit every test into a table. It is to make repeated checks compact without hiding which inputs matter, why they matter, or what a failure means.



