Translate the report into observable behavior

A report such as “the limit control ignores zero” points toward a bug, but it does not yet define a test. First, identify the input, the expected output, and the actual output. Suppose a page reads a limit from its query string. Its intended rule is that a missing, empty, malformed, or negative limit uses the default of 10, while zero is valid and means “show no items.” With ?limit=0, the page should use 0; instead, it uses 10.

Keep the first reproduction narrow. Record the exact query string, the rule it violates, and the smallest function responsible for reading it. If the report came from a browser, note whether the failure depends on browser behavior or can be reproduced in an ordinary JavaScript function. Testing the smaller function can make the failure easier to diagnose; a separate browser-level test may still be worthwhile if the interface itself has also behaved incorrectly.

Before changing code, confirm that you can observe the wrong result. A regression test is strongest when it first fails against the buggy implementation. Otherwise, a passing test might exercise a different path or merely restate what the code already does.

Build a small, repeatable reproduction

For this example, use Node.js 20 or newer, with its built-in test runner. Save the function in limit.mjs and the test in limit.test.mjs in the same directory. No browser, package installation, or network access is needed. The function accepts a query string so the test does not depend on a page’s current URL.

Here is a plausible buggy implementation for limit.mjs: export function readLimit(search) { const params = new URLSearchParams(search); return Number(params.get('limit')) || 10; }

The fallback looks convenient, but the || operator chooses its right-hand value whenever the left-hand value is falsy. Number('0') is 0, so ?limit=0 becomes 10. This explanation gives the test a precise target: preserve zero without changing the default for missing or invalid values. It also helps distinguish the bug from unrelated rendering or network issues.

Write a test that fails for the reported reason

Put the reported case in limit.test.mjs. Add a missing-value case to establish the existing default behavior. Use descriptive test names so that a failure tells you which rule broke:

import test from 'node:test'; import assert from 'node:assert/strict'; import { readLimit } from './limit.mjs'; test('accepts zero as an explicit limit', () => { assert.equal(readLimit('?limit=0'), 0); }); test('uses the default when limit is missing', () => { assert.equal(readLimit(''), 10); });

Run node --test limit.test.mjs from that directory. With the buggy function, the zero test fails because it receives 10 rather than 0; the missing-value test passes. That contrast matters. It shows that the test reaches the relevant code and that the proposed fix needs to preserve the default case.

If a test fails for another reason, such as an import error, fix the test setup before treating it as evidence of the reported bug. Do not weaken the assertion just to make the suite pass. The expected value should come from the intended behavior, not from the implementation’s current output.

Fix the cause, then test nearby boundaries

Replace the falsy fallback with an explicit check. This version distinguishes a missing value from zero and rejects values outside the stated rule:

export function readLimit(search) { const raw = new URLSearchParams(search).get('limit'); if (raw === null || raw.trim() === '') return 10; const value = Number(raw); return Number.isInteger(value) && value >= 0 ? value : 10; }

Run the same test command again. Then add cases for inputs near the failure: ?limit=1 should return 1, while ?limit=, ?limit=abc, and ?limit=-1 should each return 10. These cases protect the distinction the fix introduced. They are more useful than many variations of the same ordinary positive number.

Be explicit about any unresolved behavior. For example, URLSearchParams.get returns the first value when a key appears more than once. If repeated limit parameters matter to the interface, decide what they should mean and test that rule separately. A regression test should capture an agreed contract, not quietly invent one.

Keep the test useful after the bug is closed

Retain the failing-first test with the fix, and give reviewers enough context to connect it to the original report: ?limit=0 previously produced 10 and now produces 0. The test should continue to run with the project’s normal test command. If the application has a browser test suite, consider an additional check that the visible list is empty when the URL contains ?limit=0; that verifies the connection between parsing and display.

Resist making a single regression test responsible for every part of the page. A focused function test identifies a parsing regression quickly, while an interface test can catch broken wiring or rendering. Together they answer different questions. The goal is not simply to prove that a patch works once, but to make the original failure easy to recognize if it returns.

Back to category ↗