Lab 14: Testing with Node.js Test Runner

Objective

Write unit tests, integration tests, and use mocking with Node.js's built-in node:test module and the assert library — no external dependencies required.

Background

Testing is how professional developers prove their code works and catch regressions before production. Node.js 18+ ships with a built-in test runner (node:test) and assertion library (node:assert) that eliminate the need for external frameworks in many cases. You'll also see patterns from Jest/Vitest since they share the same mental model. Every function you've written in previous labs should have tests.

Time

45 minutes

Prerequisites

  • Labs 01–07 (Functions, OOP, Error Handling)

  • Lab 13 (Functional Programming — functions to test)

Tools

  • Node.js 20 LTS

  • Docker image: innozverse-js:latest


Lab Instructions

Step 1: Your First Test — assert and node:test

💡 assert/strict uses strict equality (===) for all comparisons. Always prefer it over the non-strict version. assert.throws() verifies that a function throws — passing the call as a lambda, not calling it directly.

📸 Verified Output:


Step 2: Organizing Tests — describe Blocks

💡 describe groups related tests under a label. This creates hierarchy in output and allows beforeEach/afterEach hooks scoped to the group. Each test creates its own Stack instance — tests must be independent.

📸 Verified Output:


Step 3: beforeEach, afterEach Hooks

Set up and tear down test state without repeating code.

💡 beforeEach runs before every single test in the describe block. It ensures tests don't share state — test A's mutations can't affect test B. This isolation is the #1 rule of testing: each test must be independent and order-independent.

📸 Verified Output:


Step 4: Testing Async Code

Testing Promises, async/await, and error rejection.

💡 Async tests must be async functions and you must await inside them. assert.rejects() is the async equivalent of assert.throws() — it awaits the rejected Promise and checks the error. Forgetting await makes tests pass trivially (they complete before the Promise resolves).

📸 Verified Output:


Step 5: Mocking — Replace Dependencies

Use node:test mock utilities to replace dependencies with controlled test doubles.

💡 Mocking replaces real dependencies (databases, email servers, HTTP clients) with controlled fakes. This lets tests run fast (no real network calls), reliably (no flaky external services), and in isolation. mock.fn() tracks every call, its arguments, and return values.

📸 Verified Output:


Step 6: Test Coverage — What to Test

💡 Test every branch. If code has an if/else, test both paths. If it throws, test the throw. A good rule: if you can delete a line of production code and all tests still pass, you have a missing test. Aim for branch coverage, not just line coverage.

📸 Verified Output:


Step 7: Test-Driven Development (TDD) — Red-Green-Refactor

Write failing tests first, then implement the code to make them pass.

💡 TDD discipline: Write the test, run it (RED — it fails), write minimum code (GREEN — it passes), clean up (REFACTOR). The test becomes your specification. This forces you to think about the API and edge cases before implementation.

📸 Verified Output:


Step 8: Running Tests — CLI and Watch Mode

💡 assert.deepEqual vs assert.equal: Arrays and objects are reference types — [1,2] === [1,2] is false (different objects). Use deepEqual for structural comparison. equal uses === which is fine for primitives.

📸 Verified Output:


Verification

Expected: All 4 RateLimiter tests pass.

Common Mistakes

Mistake
Fix

Not await-ing async tests

Async test functions must be async and awaited inside

assert.equal on arrays/objects

Use assert.deepEqual for structural comparison

Shared state between tests

Use beforeEach to create fresh instances

Not testing error paths

Every throw needs a corresponding assert.throws test

Testing implementation, not behavior

Test what functions return/do, not how they do it

Summary

You've written tests using Node's built-in node:test runner — unit tests, grouped describe blocks, beforeEach/afterEach hooks, async test handling, mocking with mock.fn(), branch coverage strategy, and TDD red-green-refactor. These skills apply directly to Jest, Vitest, and Mocha — they all share the same mental model.

Further Reading

Last updated