Start with a contract, not a list of guesses

A happy-path test checks that a feature works when everything looks normal. For a quantity field, that might mean entering "3" and expecting the number 3. It is a useful test, but it says little about empty input, the smallest permitted quantity, or values just beyond the permitted range. Those cases matter because they force the code to make decisions that typical inputs never exercise.

Before writing more tests, state what the function accepts and returns. In this example, parseQuantity accepts a string containing a whole-number quantity from 1 through 99. Surrounding whitespace is allowed, and leading zeros are allowed, so " 01 " becomes 1. Anything else, including a non-string value, returns null. Returning null is a design choice for this example; a different application might use an error object or throw an exception. Tests should enforce the contract you actually choose, not an unstated assumption.

Keep the function separate from the form or button that calls it. A small, deterministic parsing function is easy to test without constructing a page or simulating clicks. Interface tests can then check that the UI passes input to the function and displays the result appropriately.

Choose values on both sides of each boundary

For the permitted range, test 1 and 99 because they are valid limits. Then test 0 and 100 because they sit immediately outside those limits. Include one ordinary value, such as 3, to confirm that the function handles more than edge cases. This pattern catches mistakes such as using greater than instead of greater than or equal to, or accidentally excluding the maximum.

There are boundaries in the input format as well as in the numeric range. Test an empty string and a string containing only spaces; trimming must not turn either into a valid quantity. Test a decimal such as "1.5", a negative value such as "-1", and a string with extra characters such as "3 items". If the contract permits leading zeros and surrounding spaces, test those as valid inputs rather than treating every unusual-looking string as invalid.

Types deserve their own cases. A browser text input normally provides a string, but the parsing function might also be called from another component or a test helper. Passing 3, null, or undefined should return null under this contract, not crash when the function tries to trim the value. These tests document what happens when a caller breaks the expected input shape.

Make the example reproducible

The following example needs Node.js 20 or newer and no third-party packages. Save the text below as quantity.test.mjs, then run node --test quantity.test.mjs in the same directory. The function and tests share one file to keep the example small; in a larger project, the function would usually live in a separate module.

import test from 'node:test'; import assert from 'node:assert/strict'; function parseQuantity(input) { if (typeof input !== 'string') return null; const text = input.trim(); if (!/^[0-9]+$/.test(text)) return null; const value = Number(text); return Number.isSafeInteger(value) && value >= 1 && value <= 99 ? value : null; } const validCases = [ ['1', 1], ['3', 3], ['99', 99], [' 01 ', 1] ]; const invalidCases = [ '0', '100', '', ' ', '1.5', '-1', '3 items', '999999999999999999999', 3, null, undefined ]; test('accepts quantities within the contract', () => { for (const [input, expected] of validCases) { assert.equal(parseQuantity(input), expected, String(input)); } }); test('rejects values outside the contract', () => { for (const input of invalidCases) { assert.equal(parseQuantity(input), null, String(input)); } });

The digit check rejects signs, decimal points, and trailing text before conversion. It also rejects an empty string, which Number would otherwise convert to zero. The range check rejects quantities outside 1 through 99. Number.isSafeInteger makes the integer requirement explicit, although the upper limit already rules out large values in this particular function.

Use failures to improve the contract and the tests

A passing suite does not prove that every input is covered. To check whether the boundary tests can detect a plausible bug, temporarily change value >= 1 to value > 1 and run the tests again. The case for "1" should fail. Change value <= 99 to value < 99 and the case for "99" should fail. Restore the correct operators afterward. This quick exercise shows why testing a limit itself is different from testing only a value near it.

When a real bug appears, add the smallest input that reproduces it and decide whether the contract needs clarification. For example, should "+3" be accepted? Should a Unicode digit count as a digit? The sample rejects both, but those decisions may differ elsewhere. Write down the intended behavior before changing the implementation, then add a test for it.

Finally, keep each test focused on behavior a reader can name. Grouping related cases is convenient, but an assertion message should still identify the failing input. Boundary tests are most valuable when a failure points directly to a broken rule: accepted limits, rejected neighbors, permitted formatting, or invalid types. That makes the suite useful both when writing the function and when changing it later.

Back to category ↗