npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

easy-to-test-code

v1.0.0

Published

A demonstration of testable code design with dependency injection

Readme

Code That's Easy to Test

A demonstration of testable code design principles, inspired by The Pragmatic Programmer's advice on writing "Code That's Easy to Test."

Overview

This project showcases a Notification Service built with testability as a first-class concern. It demonstrates how dependency injection, pure functions, and interface-based design make code easy to test, maintain, and reason about.

Tech Stack

  • Node.js + TypeScript
  • Jest for testing
  • Pure dependency injection (no frameworks)

Key Design Principles

1. Dependency Injection

Problem: Hard-coded dependencies (like Date.now(), Math.random(), or HTTP libraries) make code impossible to test in isolation.

Solution: All external dependencies are injected as interfaces.

// ❌ BAD: Hard-coded dependencies
class NotificationService {
  sendNotification() {
    const now = Date.now(); // Can't control time in tests!
    const delay = Math.random(); // Can't predict randomness!
    await fetch(url); // Can't mock HTTP calls easily!
  }
}

// ✅ GOOD: Dependencies injected
class NotificationService {
  constructor(
    private clock: Clock,
    private random: Random,
    private httpClient: HttpClient
  ) {}
  
  sendNotification() {
    const now = this.clock.now(); // Fully controllable in tests!
    const delay = this.random.random(); // Predictable in tests!
    await this.httpClient.request(...); // Easy to mock!
  }
}

Benefits:

  • Testability: Replace real implementations with fakes/mocks
  • Flexibility: Swap implementations without changing core logic
  • Test speed: Fake clock doesn't actually wait, tests run instantly
  • Determinism: Predictable random values enable exact assertions

2. Interface Segregation

Dependencies are defined as interfaces, not concrete classes:

interface Clock {
  now(): number;
  sleep(ms: number): Promise<void>;
}

This allows:

  • Multiple implementations: SystemClock for production, FakeClock for testing
  • Loose coupling: Service doesn't know or care about implementation details
  • Easy mocking: Create test doubles that only implement what's needed

3. Pure Functions Where Possible

Some functions have no side effects and are deterministic:

// Pure function: no side effects, predictable output
private calculateBackoffDelay(attempt: number): number {
  const exponentialDelay = this.config.baseRetryDelayMs * Math.pow(2, attempt);
  const jitter = this.random.random() * this.config.baseRetryDelayMs;
  return Math.min(exponentialDelay + jitter, this.config.maxRetryDelayMs);
}

Benefits:

  • Easier to test: No need to set up complex state
  • Easier to reason about: Input → Output, no hidden behavior
  • Reusable: Can be extracted and tested independently

4. Test Doubles (Fakes, Not Just Mocks)

We use fakes - working implementations with simplified behavior:

class FakeClock implements Clock {
  private currentTime = 1000;
  
  now() { return this.currentTime; }
  async sleep(ms: number) { this.currentTime += ms; } // Instant, controllable
  
  advance(ms: number) { this.currentTime += ms; } // Test helper
}

Why fakes over mocks?

  • More realistic: Actually implement the interface contract
  • Easier to maintain: Don't need to mock every method call
  • Better for integration tests: Can use real implementations alongside fakes

Project Structure

src/
├── interfaces/          # Dependency interfaces
│   ├── Clock.ts
│   ├── Random.ts
│   └── HttpClient.ts
├── implementations/     # Real implementations
│   ├── SystemClock.ts
│   └── SystemRandom.ts
├── types/
│   └── Notification.ts
├── NotificationService.ts  # Core service
└── __tests__/
    ├── fakes/           # Test doubles
    │   ├── FakeClock.ts
    │   ├── FakeRandom.ts
    │   └── FakeHttpClient.ts
    ├── NotificationService.test.ts      # Unit tests
    └── NotificationService.integration.test.ts  # Integration test

Running Tests

# Install dependencies
npm install

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage
npm run test:coverage

Example: Testing Retry Logic

Without dependency injection, testing retry logic with exponential backoff is painful:

// ❌ Hard to test
it('should retry with exponential backoff', async () => {
  // How do you test this without waiting real seconds?
  // How do you verify the exact delay without flakiness?
  await service.sendNotification(...);
});

With dependency injection, it's straightforward:

// ✅ Easy to test
it('should retry with exponential backoff', async () => {
  const clock = new FakeClock(1000);
  const random = new FakeRandom();
  random.setSequence([0.1, 0.2]); // Predictable jitter
  
  httpClient.setError(new Error('Fail'));
  httpClient.setError(new Error('Fail'));
  httpClient.setResponse({ status: 200 });
  
  await service.sendNotification(notification);
  
  // Verify exact timing without waiting
  expect(clock.getTime()).toBe(3300); // Exact assertion!
});

What Makes This Code Easy to Test?

  1. No Hidden Dependencies: Everything is explicit in the constructor
  2. Controllable Time: Fake clock allows testing time-dependent logic instantly
  3. Deterministic Randomness: Fake random enables exact assertions
  4. Isolated HTTP: Fake HTTP client enables testing without network calls
  5. Pure Functions: Backoff calculation is testable in isolation
  6. Clear Interfaces: Well-defined contracts make fakes easy to create

Connection to "The Pragmatic Programmer"

This code follows several principles from The Pragmatic Programmer:

  • "Design by Contract": Interfaces define clear contracts
  • "Decoupling": Dependencies are injected, not hard-coded
  • "Orthogonality": Pure functions are independent and reusable
  • "Reversibility": Can swap implementations without changing core logic
  • "Testing": "Test early, test often, test automatically" - made possible by testable design

The book emphasizes that code should be easy to test, not testable as an afterthought. By designing with testability in mind from the start, we get:

  • Faster tests (no real I/O)
  • More reliable tests (deterministic)
  • Easier debugging (isolated, controlled)
  • Better design (loose coupling, clear contracts)

Real-World Application

While this is a demo, these patterns are used in production systems:

  • Microservices: Services often need to test timeouts, retries, rate limiting
  • API clients: Need to test error handling, retries, circuit breakers
  • Scheduled jobs: Need to test timing logic without waiting
  • Event systems: Need to test ordering and timing

Next Steps

  • Add more test scenarios (circuit breakers, rate limiting edge cases)
  • Implement a real HTTP client (e.g., using node-fetch)
  • Add performance tests
  • Explore property-based testing with deterministic random

License

MIT