Skip to content

JavaScript Testing

Test module exports, function signatures, and observable behaviors — not implementation details. If tests run in Node.js, use node:assert/strict for assertions.

The pattern

Test module exports explicitly

Verify modules export what they claim with correct types:

javascript
import { test } from "node:test";
import assert from "node:assert/strict";
import {
  processData,
  validateInput,
  DEFAULT_CONFIG,
} from "./data-processor.js";

test("exports required functions", () => {
  assert.strictEqual(typeof processData, "function");
  assert.strictEqual(typeof validateInput, "function");
  assert.strictEqual(typeof DEFAULT_CONFIG, "object");
});

Verify:

  • Every public export has an explicit existence test
  • Export types match expected interface contracts

Test function signatures before behavior

Validate function names and return types before testing behavior:

javascript
import { describe, test } from "node:test";
import assert from "node:assert/strict";
import { calculateTotal } from "./math-utils.js";

describe("calculateTotal function", () => {
  test("has correct name", () => {
    assert.strictEqual(
      calculateTotal.name,
      "calculateTotal",
    );
  });
  
  test("returns number", () => {
    const result = calculateTotal([1, 2]);
    assert.strictEqual(typeof result, "number");
  });
  
  test("calculates sum correctly", () => {
    assert.strictEqual(calculateTotal([1, 2, 3]), 6);
  });
});

Verify:

  • Function name matches expected identifier
  • Return type matches expected interface

One test per observable behavior

Focus tests on externally visible outcomes rather than internal implementation.

DON'T test implementation details:

javascript
test("uses quicksort algorithm", () => {
  const spy = sinon.spy(sortUsers, "_quicksort");
  sortUsers([{ name: "Bob" }, { name: "Alice" }]);
  assert.ok(spy.called);
});

DO test observable behavior:

javascript
test("sorts users by name alphabetically", () => {
  const users = [
    { name: "Bob" },
    { name: "Alice" },
  ];
  const expected = [
    { name: "Alice" },
    { name: "Bob" },
  ];
  const actual = sortUsers(users);
  assert.deepStrictEqual(actual, expected);
});

Define expected and actual before comparing

Extract values into named constants for clarity and easier debugging:

DON'T inline values in assertions:

javascript
test("calculates discount", () => {
  assert.strictEqual(applyDiscount(100, 0.2), 80);
});

DO define expected and actual constants:

javascript
test("calculates discount", () => {
  const expected = 80;
  const actual = applyDiscount(100, 0.2);
  assert.strictEqual(actual, expected);
});

Named constants make test failures easier to diagnose and encourage thinking about the expected outcome before running code.

Exception: Don't use this pattern for boolean checks. Use assert.ok when testing truthiness:

javascript
// Good — assert.ok for boolean conditions
assert.ok(fs.existsSync(outputPath));
assert.ok(results.length > 0);

// Unnecessary — don't wrap booleans in actual/expected
const expected = true;
const actual = fs.existsSync(outputPath);
assert.strictEqual(actual, expected);

Choose assertion methods by value type

Use strictEqual for primitives and deepStrictEqual for objects/arrays:

javascript
// Primitives — use strictEqual
const expected = 42;
const actual = calculateTotal(items);
assert.strictEqual(actual, expected);

// Objects/Arrays — use deepStrictEqual
const expected = { name: "Alice", role: "admin" };
const actual = getUser("alice");
assert.deepStrictEqual(actual, expected);

Never reach inside actual and expected

Once you have expected and actual, compare them directly. Never access their properties in assertions:

DON'T reach inside actual and expected:

javascript
test("returns config object", () => {
  const expected = {
    supervisor: cfg1,
    server: cfg2,
    db: cfg3,
    worktree: "/tmp",
  };
  const actual = getConfig();
  // Wrong — reaching inside actual and expected
  assert.deepStrictEqual(
    actual.supervisor,
    expected.supervisor,
  );
  assert.deepStrictEqual(
    actual.server,
    expected.server,
  );
  assert.deepStrictEqual(actual.db, expected.db);
  assert.strictEqual(
    actual.worktree,
    expected.worktree,
  );
});

DO compare actual and expected directly:

javascript
test("returns config object", () => {
  const expected = {
    supervisor: cfg1,
    server: cfg2,
    db: cfg3,
    worktree: "/tmp",
  };
  const actual = getConfig();
  assert.deepStrictEqual(actual, expected);
});

If you're reaching into properties, either assign the right values to actual and expected in the first place, or use deepStrictEqual to compare the full objects.