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.
| Concept | What it means | Decision rule |
|---|---|---|
| Unit test | Fast proof of one behavior in isolation | Use for branching domain and use-case rules |
| Contract test | The same behavioral suite applied to adapters | Use to prove fakes and real adapters agree |
| Test double | Controlled replacement for a dependency | Fake at owned ports, not arbitrary internals |
Professional workflow
Build and verify Node.js programs from the terminal in small, observable steps.
- Define the test strategy boundary: inputs, outputs, invariants, ownership, and expected failures.
- Design the data or message contract before choosing implementation details.
- Implement the smallest correct path with dependencies passed explicitly.
- Add validation, failure translation, cleanup, and concurrency behavior.
- Verify the boundary with realistic data and at least one adversarial case.
- Measure or observe the behavior before optimizing or extracting abstractions.
Keep the feedback loop short
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.
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
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
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