Unit Test Authoring Guide
Language: JavaScript
The complete rule set for writing unit tests. Follow it exactly and any reviewer (or AI agent) will recognize the result as a test on sight. For testing strategy across modules see testing-strategy.md. For testing tiers and CI/CD see module-testing.md.
On This Page
- Tools
- File Structure
- Test File Template
- Mandatory Rules
- How to Write Tests for a New Module
- Reference - Assertion Methods
- Reference - Test Output
Tools
- Runner: Node.js built-in test runner (
node --test) - Assertions:
require('node:assert/strict') - Test API:
describeanditfromrequire('node:test') - No external dependencies. No Mocha, Jest, Chai, or Sinon. Only Node.js built-ins.
File Structure
[module-name]/
_test/
test.js # All tests for this module
mock-data/ # (Optional) JSON fixtures2
3
4
Tests run from the module root:
npm testThe module's package.json must have:
"scripts": {
"test": "node --test _test/test.js"
}2
3
Test File Template
// Tests for [module-name]
// Covers all exported functions with automated assertions
'use strict';
const assert = require('node:assert/strict');
const { describe, it } = require('node:test');
const ModuleName = require('../module-file.js');
// ============================================================================
// 1. [CATEGORY NAME]
// ============================================================================
describe('[functionName]', function () {
it('should [expected result] when [input condition]', function () {
assert.strictEqual(ModuleName.functionName(input), expectedOutput);
});
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Mandatory Rules
1. One describe block per exported function
Each exported function gets its own describe block. The describe label must match the function name exactly.
describe('isNull', function () { ... });2. Test naming convention
Every it() must follow this pattern:
should [expected result] when [input condition]Examples:
it('should return true when value is null', ...)
it('should return false when value is empty string', ...)
it('should return 11.02 when rounding 11.01999999 to 2 decimals', ...)2
3
3. One assertion per test where possible
Each it() block should ideally test one specific input → output pair. Multiple assertions in one it() are allowed only when testing a single logical concept (e.g., checking multiple fields of a returned object).
4. Group by category with numbered section headers
Use comment separators to group related functions:
// ============================================================================
// 1. TYPE CHECKS
// ============================================================================
// ============================================================================
// 2. STRING OPERATIONS
// ============================================================================2
3
4
5
6
7
Categories should follow the logical grouping of the module's API.
5. Test every exported function
Every public function in the module must have at least one describe block with tests. No exceptions.
6. Include edge cases from the source code
Read the function implementation. Look for:
- Null/undefined handling - does the function check for null? Test it.
- Type coercion - does the function convert types? Test boundary cases.
- Empty inputs - empty string, empty array, empty object.
- Boundary values - min/max of ranges, zero, negative numbers.
- Return types - does it return
falsevsnullvsundefined? Test each.
7. Use strictEqual for primitives, deepStrictEqual for objects
// Primitives (string, number, boolean, null, undefined)
assert.strictEqual(result, expected);
// Objects, arrays
assert.deepStrictEqual(result, expected);2
3
4
5
8. No console.log in tests
All verification is via assertions. Tests must pass or fail automatically - no manual inspection of output.
9. Test inputs and expected outputs must be explicit
Do not use variables that obscure what is being tested:
// Input and expected output are visible in the test
it('should return 11.02 when rounding 11.01999999 to 2 decimals', function () {
assert.strictEqual(Utils.round(11.01999999, 2), 11.02);
});
// Wrong - hidden behind variable names
it('should round correctly', function () {
assert.strictEqual(Utils.round(testInput, decimals), expectedResult);
});2
3
4
5
6
7
8
9
10. No mutation side effects between tests
Each it() must be independent. Do not rely on state from a previous test. If setup is needed, declare it inside the describe block.
How to Write Tests for a New Module (Step by Step)
- Read the module source file. List every exported function.
- Group functions by category (type checks, string operations, validation, etc.).
- For each function: a. Read the implementation and JSDoc comments b. Identify all code paths (if/else branches, try/catch, loops) c. Write one
it()for each code path d. Add edge cases: null, undefined, empty string, empty object, empty array, NaN, zero, negative - Check for existing handwritten tests in
_cleanup/_old/- convert anyconsole.logtests to assertions. - Run the tests:
npm test - Verify all pass with zero failures. Fix any assertion mismatches by checking the actual function behavior - do not change the function to match the test.
Reference - Assertion Methods
| Method | Use for |
|---|---|
assert.strictEqual(actual, expected) | Primitives: string, number, boolean, null, undefined |
assert.deepStrictEqual(actual, expected) | Objects, arrays (deep comparison) |
assert.ok(value) | Truthy check |
assert.throws(fn) | Function should throw an error |
Reference - Test Output
Running npm test produces output like:
▶ isNull
✔ should return true when value is null (0.5ms)
✔ should return false when value is undefined (0.03ms)
✔ isNull (1.2ms)
ℹ tests 211
ℹ suites 48
ℹ pass 211
ℹ fail 02
3
4
5
6
7
8
9
Each function is a suite. Each it() is a test. All results are visible in the terminal.
Further Reading
- Testing Strategy - directory layout and the simulating-loader pattern
- Module Testing - testing tiers (emulated vs integration) and CI/CD
- Integration Testing - testing helpers against real cloud services