Explorer
JavaScript

Testing JavaScript

JavaScript Theory & Concepts

Testing JavaScript

The Testing Pyramid, AAA pattern, test runners, mocks, and behavior-driven verification.

πŸ“– The Story & Real-World Analogy

The Automotive Safety Crash Test Facility

"Before a newly designed vehicle is released to highway drivers, automobile engineers do not just "cross their fingers" and hope the brakes work. They test individual parts on hydraulic test benches (Unit Tests: testing the brake calipers in isolation), test how the transmission couples with the engine (Integration Tests), and run the fully assembled car through a crash test track with test dummies (End-to-End / E2E Tests). Testing code gives you that same highway confidence!"

Automated testing ensures code correctness, prevents regressions, and enables fearless refactoring across complex codebases.

βš™οΈ How It Works Under The Hood (Step-by-Step)
1

The Testing Pyramid

Unit Tests (fast, isolated, plentiful) at the base β†’ Integration Tests (verifying connected modules) in the middle β†’ End-to-End Tests (real browser flows, slowest, highest fidelity) at the top.

2

The AAA Pattern (Arrange, Act, Assert)

Structure every test cleanly: 1. Arrange (set up inputs and mocks), 2. Act (execute the target function), 3. Assert (verify results match expectations).

3

Mocks, Spies, and Stubs

Isolate units from external systems (databases, timers, HTTP calls) by replacing real dependencies with controllable test doubles.

4

Test-Driven Development (TDD)

The Red-Green-Refactor cycle: write a failing test first (Red) β†’ write minimal code to pass (Green) β†’ refactor with confidence.

πŸ’» Interactive Code Walkthrough

Writing an isolated unit test following the AAA pattern with custom assertions:

JAVASCRIPT
// Target function to test
function calculateDiscount(price, isVip) {
  if (price < 0) throw new RangeError("Price cannot be negative");
  const discountRate = isVip ? 0.2 : 0.05;
  return price - (price * discountRate);
}

// Mini Test Runner Simulation
function test(name, fn) {
  try {
    fn();
    console.log(`βœ… PASS: ${name}`);
  } catch (err) {
    console.error(`❌ FAIL: ${name}\n   ${err.message}`);
  }
}

function expect(actual) {
  return {
    toBe(expected) {
      if (actual !== expected) throw new Error(`Expected ${expected}, but got ${actual}`);
    },
    toThrow(errorType) {
      // verified in try/catch block
    }
  };
}

// Executing Tests
test("calculates 20% discount for VIP customers", () => {
  // 1. Arrange
  const price = 100;
  const isVip = true;

  // 2. Act
  const finalPrice = calculateDiscount(price, isVip);

  // 3. Assert
  expect(finalPrice).toBe(80);
});

test("calculates 5% discount for regular customers", () => {
  expect(calculateDiscount(100, false)).toBe(95);
});
Console Output:
CODE
βœ… PASS: calculates 20% discount for VIP customers
βœ… PASS: calculates 5% discount for regular customers
⚠️ Common Pitfalls & Interview Traps
Trap
Testing Implementation Details Instead of Behavior

The Risk: Writing tests that assert internal private variable names or exact loop counters makes tests brittleβ€”they break on harmless refactors even when functionality is intact.

The Fix: Test observable behavior: test inputs vs outputs from the consumer's perspective.

⚑ 30-Second Quick Revision Cheat Sheet (TL;DR)
  • βœ“ Testing Pyramid: Many fast Unit tests, moderate Integration tests, few E2E tests.
  • βœ“ Follow the AAA pattern: Arrange β†’ Act β†’ Assert.
  • βœ“ Test behavior and contracts, not internal implementation details.
  • βœ“ Mock external dependencies (HTTP, timers, DB) to keep tests deterministic and fast.
  • βœ“ A good test suite gives confidence to refactor without fear of regressions.

Finished this lesson?

Mark this chapter complete to update your learning streak and unlock the next lesson.