Phase 6 · Testing, Delivery & ProductionModule 41~66 min read

Advanced Testing with Node's Test Runner

Design a balanced test portfolio with node:test suites, hooks, mocks, fixtures, coverage, property checks, and deterministic concurrency.

What you'll learn

A professional suite spends fast feedback on pure rules and realistic evidence on boundaries. Use Node's test runner for isolated units, controlled fakes, contract suites, coverage insight, and deterministic async behavior.

By the end of this lesson, you'll be able to:

  • Design a balanced test portfolio
  • Use suites, hooks, mocks, and factories
  • Test time and concurrency deterministically
  • Interpret coverage as a signal

Core mental model

Node.js becomes easier when you separate the JavaScript language from the runtime and the operating-system capabilities it exposes. Use this table as a decision guide.

ConceptWhat it meansDecision rule
Unit testFast proof of one behavior in isolationUse for branching domain and use-case rules
Contract testThe same behavioral suite applied to adaptersUse to prove fakes and real adapters agree
Test doubleControlled replacement for a dependencyFake at owned ports, not arbitrary internals

Professional workflow

Build and verify Node.js programs from the terminal in small, observable steps.

  1. Define the test strategy boundary: inputs, outputs, invariants, ownership, and expected failures.
  2. Design the data or message contract before choosing implementation details.
  3. Implement the smallest correct path with dependencies passed explicitly.
  4. Add validation, failure translation, cleanup, and concurrency behavior.
  5. Verify the boundary with realistic data and at least one adversarial case.
  6. Measure or observe the behavior before optimizing or extracting abstractions.

Keep the feedback loop short

Run the smallest useful command after every meaningful change. Read the complete error message before editing again, and keep inputs and outputs visible while you learn.

Guided code lab

Test behavior with explicit dependencies

A fixed clock and in-memory port remove time and I/O nondeterminism without mocking implementation details.

create-task.test.js
import test from 'node:test';
import assert from 'node:assert/strict';

test('creates a task with server-owned identity and time', async () => {
  const tasks = new InMemoryTasks();
  const createTask = makeCreateTask({ tasks, ids: { next: () => 't1' }, clock: { now: () => fixedDate } });
  const task = await createTask(subject, { title: 'Test deeply' });
  assert.deepEqual(task, { id: 't1', ownerId: subject.id, title: 'Test deeply', createdAt: fixedDate });
});

Production practice

Contract

Each test states behavior, arranges only relevant inputs, asserts public outcomes and important effects, and owns cleanup.

Verification

Run in random and parallel order, detect open handles, repeat race-prone tests, mutation-test critical rules, and inspect missed coverage.

Operations

Keep the default suite fast, quarantine nothing silently, record flaky-test ownership, and publish test/coverage artifacts in CI.

Common failure mode

Mocking every internal call couples tests to implementation and lets the real database, network, and serialization boundaries remain unverified.

Independent workshop

Build a layered test portfolio for the secure task API.

Your finished workshop must include:

  • Risk-based test map
  • Use-case unit tests
  • Adapter contract suite
  • Factories and fixtures
  • Deterministic time
  • Coverage review

Definition of done

Run the happy path and at least two edge cases, keep responsibilities separated, and add a short README explaining how to run the program.

Recap & quick check

Key takeaways

  • Tests buy confidence
  • Boundaries deserve realism
  • Fakes implement owned ports
  • Time is a dependency
  • Coverage is not correctness

Quick check

1. What should unit tests emphasize?

2. Why contract-test adapters?

3. What does high coverage guarantee?

Next: Contract, End-to-End & Resilience Testing