Safeguard
Application Security•

How do you write a unit test in JavaScript with the built-in runner?

Node.js ships a test runner. Write a unit test with node:test and assert, mock a dependency and run it with no extra packages. Includes a security case.

Safeguard Security Team
5 min read

To write a unit test in JavaScript, import test from node:test and assert from node:assert, call test with a name and a function, and make assertions inside it. Run the file with Node. The test passes if the function finishes without throwing and fails if it throws or rejects.

What does the smallest test look like?

The Node.js documentation says tests created with the test module consist of a single function that is processed in one of three ways: a synchronous function that fails if it throws, a function returning a promise that fails if the promise rejects, or a callback style function. Save this as add.test.js:

import test from 'node:test';
import assert from 'node:assert/strict';

function add(a, b) { return a + b; }

test('adds two numbers', () => {
  assert.equal(add(2, 3), 5);
});

Run it with node add.test.js. The runner prints a report of passing and failing tests. No package install is needed.

How do you group tests?

The docs describe describe and it as aliases for suite and test, so you can write them in the style used by popular frameworks:

import { describe, it } from 'node:test';
import assert from 'node:assert/strict';

describe('price rules', () => {
  it('rejects negative quantities', () => {
    assert.throws(() => price(-1), /quantity/);
  });
});

Subtests are supported too, which lets you build a table of cases inside one test. The documentation also lists skipping tests, marking tests as pending, and filtering by name.

How do you test async code and mock a dependency?

An async function that returns a promise works directly: if it rejects, the test fails. For dependencies, the module includes a mock facility, with functions such as mock.fn and mock.method that replace an implementation and record calls.

import test, { mock } from 'node:test';
import assert from 'node:assert/strict';

test('notifies on failure', async () => {
  const send = mock.fn(async () => {});
  await handlePayment({ ok: false }, { send });
  assert.equal(send.mock.callCount(), 1);
});

Pass the dependency in as an argument, as above. Code that can receive its collaborators is far easier to test than code that imports them privately.

What should a unit test not do?

Avoid network calls, real clocks and shared state. The docs also include mock timers, so you can control time instead of waiting for it. Keep each test independent, so order does not matter and a failure points to one cause.

Why include security cases?

Unit tests are cheap places to pin down security rules. A constructed example: a function builds a file path from a user supplied name. The team writes tests that pass normal names and also a name containing parent directory segments, and asserts the function throws. Later, when someone refactors the function, the test fails if the check disappears.

Write one test per rule you care about: input validation limits, permission checks, escaping of output. These tests double as documentation of what the code must never allow.

How do you organize tests as a project grows?

Keep files small and named after the unit under test. The Node.js documentation covers running tests from the command line, collecting code coverage and choosing reporters, so you can add those when the project needs them. Start simple: one test file per module, and a single script in package.json that runs them all.

Separate fast unit tests from slower integration tests that need a database or a network. Run the fast ones on every save and the slow ones in the pipeline. A suite that takes a minute gets skipped. A suite that takes two seconds gets run constantly.

What are common mistakes?

Testing implementation instead of behavior is the biggest. If a refactor that keeps the output the same breaks ten tests, the tests are too tightly coupled. Another is asserting nothing: a test that runs the code but never checks a result passes forever. A third is forgetting to await an async call, so the test finishes before the work does. Make sure async tests return or await their promise.

How to check or apply this

  1. Put test files next to the code, named with a test suffix, and import from node:test.
  2. Use the strict assert module so comparisons do not coerce types.
  3. Write one behavior per test, with a name that reads as a sentence.
  4. Inject dependencies as arguments and replace them with mock.fn in tests.
  5. Add a failing test first for every bug you fix.
  6. Add security cases: bad input, missing permission, oversized values.
  7. Run the tests in your build pipeline and fail the build on any failure.
  8. Use the runner's watch mode while developing to rerun affected tests whenever a file changes.

Quick check

Do I need Jest or another framework?

Not for basic unit testing. Node's built-in runner covers tests, suites, mocks and reporting. Teams choose other frameworks for extra features or existing habits.

How does the runner know a test failed?

A synchronous test fails if it throws, and an async test fails if its promise rejects. A failed assertion throws, so it fails the test.

Sources

  1. Node.js documentation, Test runner

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.