3 · Connect, test and build · 18 MIN
Write repeatable tests for the logic
A test should check behavior, including boundaries and failures.
Manual checks are useful but easy to forget. Node’s built-in test runner executes named tests and reports failures. Extract pure logic from HTTP handlers so it can be tested with simple values. deepEqual compares the structure of arrays and objects; equal suits primitive values. This example tests filtering and a zero limit. It also checks that the input array is unchanged. Add separate tests for each meaningful contract instead of copying the function’s implementation into the test.
Run node --test lesson.mjs in the terminal. Read each test name before its assertion. Add one edge case per test. Break the function, observe a red result, then restore it.
Before you start
Install Node.js 22.12+ (or a supported newer LTS). Create a folder named codingneed-node. Open a terminal in that folder. Save ONE lesson example as lesson.mjs and run node lesson.mjs. No npm dependencies are required. HTTP lessons keep running until you press Ctrl+C; stop the previous server before starting another.
File for this example: lesson.mjs
New words, explained
- testRunner
- A program that executes tests and reports their outcomes.
- pureFunction
- A calculation depending on its inputs without external side effects.
- regression
- A previously working behavior broken by a change.
Follow the example step by step
- Run node --test lesson.mjs in the terminal.
- Read each test name before its assertion.
- Add one edge case per test.
- Break the function, observe a red result, then restore it.
You are ready to move on when: All five behavior tests pass after restoring the implementation. An intentional filter bug causes a failure. Explain why helper and HTTP contracts can have different allowed inputs.
Read the example
import test from 'node:test';
import assert from 'node:assert/strict';
function findTopics(topics, query, limit) {
return topics.filter(topic => topic.toLowerCase().includes(query.trim().toLowerCase())).slice(0, limit);
}
test('matches case and ignores surrounding spaces', () => {
assert.deepEqual(findTopics(['React', 'SQL'], ' sql ', 2), ['SQL']);
});
test('zero limit returns nothing without mutating the list', () => {
const topics = ['React'];
assert.deepEqual(findTopics(topics, '', 0), []);
assert.deepEqual(topics, ['React']);
});Check the expected output
Run node --test lesson.mjs. Both named tests pass. This helper accepts a zero limit, unlike the previous HTTP endpoint’s stricter contract.
Your challenge
Add tests for an empty list, no matching topic and applying a limit after filtering. Deliberately introduce a bug and confirm one of your tests fails.
Solution cost: Discuss the operations in this small example; rendering and I/O costs depend on the host. time · Proportional to the example’s retained data. space
Common trap
Passing tests demonstrate the cases checked; they do not prove every possible input is correct.
Study the project implementation
import test from 'node:test';
import assert from 'node:assert/strict';
function findTopics(topics, query, limit) {
return topics.filter(topic => topic.toLowerCase().includes(query.trim().toLowerCase())).slice(0, limit);
}
test('matches case and ignores surrounding spaces', () => {
assert.deepEqual(findTopics(['React', 'SQL'], ' sql ', 2), ['SQL']);
});
test('zero limit returns nothing without mutating the list', () => {
const topics = ['React'];
assert.deepEqual(findTopics(topics, '', 0), []);
assert.deepEqual(topics, ['React']);
});Further reading: Official documentation
Next lesson: Project · A small searchable topics API →