What a unit test checks

A unit test checks one small piece of a program, such as a function, by giving it an input and comparing its output with an expected result. It helps you notice when a change breaks behavior you intended to keep. A passing test does not prove that a function works for every possible input; it confirms only the cases you checked.

In this example, you will test an add function. You need a text editor, a terminal, and Node.js version 20 or later. The example uses Node.js’s built-in test runner and assertion library, so there is no package to install. The files use the .mjs extension so Node.js treats them as JavaScript modules.

Create the function and its test

Make a new folder for the exercise. In that folder, create a file named math.mjs with this content: export function add(a, b) { return a + b; }

The export keyword makes add available to another file. For now, the function’s intended behavior is simple: given two numbers, it returns their sum. That description gives us a clear expectation to test.

In the same folder, create math.test.mjs with this content: import test from 'node:test'; import assert from 'node:assert/strict'; import { add } from './math.mjs'; test('adds two numbers', () => { assert.equal(add(2, 3), 5); });

The import statements bring in the test runner, an assertion method, and your function. The test call gives the case a readable name. Inside it, assert.equal compares the actual value returned by add(2, 3) with the expected value, 5. If they differ, the assertion fails.

Run the test and check a failure

Open a terminal in the folder containing both files and run node --test math.test.mjs. Node.js will report whether the test passed or failed. If it cannot find a file, check its name, extension, and location. The import path './math.mjs' is relative to math.test.mjs, so both files need to be in the same folder for this example.

To see what a useful failure looks like, temporarily change the expected value in the test from 5 to 6, then run the command again. The test should fail because add(2, 3) still returns 5. Read the reported actual and expected values, then restore the expectation to 5 and rerun the test. A failure is information: it points you toward either an incorrect function or an incorrect expectation.

Add cases that describe the behavior

One passing example is a start, but it leaves many inputs unchecked. Add these tests below the first one in math.test.mjs: test('adds zero', () => { assert.equal(add(7, 0), 7); }); test('adds a negative number', () => { assert.equal(add(7, -2), 5); });

Run node --test math.test.mjs again. These cases check two useful boundaries of the behavior you described: adding zero leaves a number unchanged, and adding a negative number reduces the result. Give each test a name that says what it verifies, so a failure is easier to understand later.

Keep the function’s contract in mind when choosing cases. JavaScript’s + operator also joins strings, so add('2', '3') would not behave like numeric addition. If your program must accept only numbers, that is a separate requirement: you would need to define how invalid inputs should be handled, implement that behavior, and then test it. Do not assume these three tests cover input validation.

Use tests as a small feedback loop

A practical testing routine is to state the behavior you want, write a test for one representative case, run it, and inspect the result. When you change the function, run the tests again. If one fails, compare its expected value with what the function actually returned before changing either one.

Keep unit tests focused on observable behavior rather than the exact steps a function uses internally. That way, you can improve the implementation without rewriting tests that still describe the right outcome. For this exercise, the three assertions form a small, repeatable check of add—not a guarantee that every possible use of the function is safe.

Back to category ↗