Course contentsShow
← Node.js
Lesson 1524 of 1,98014. Testing Methodologies and Quality AssuranceFree lesson

Dependency Injection for Testability

Refactor application code to use dependency injection, making components easier to isolate during unit testing.

In this lesson, you will learn how to decouple your Node.js components by implementing Dependency Injection (DI) to make your code inherently more testable.

The Problem: Hard-Coded Dependencies

When a class or module is responsible for instantiating its own dependencies (like a database client or an API wrapper), it becomes "tightly coupled." During unit testing, you are forced to deal with those real dependencies, which makes tests slow, non-deterministic, or physically impossible to run without a live infrastructure.

Consider this problematic code:

// user-service.js
const db = require('./database'); // Direct dependency

class UserService {
  async getUser(id) {
    return await db.query('SELECT * FROM users WHERE id = $1', [id]);
  }
}

Because db is imported directly inside the module, you cannot easily swap it for a mock object when testing UserService.

The Solution: Injecting Dependencies

Dependency Injection shifts the responsibility of creating a dependency to the caller. By passing the dependency through the constructor or a method argument, you can easily provide a "mock" implementation during testing without modifying the business logic.

// user-service.js (Refactored)
class UserService {
  constructor(database) {
    this.db = database; // Dependency is injected
  }

  async getUser(id) {
    return await this.db.query('SELECT * FROM users WHERE id = $1', [id]);
  }
}

Now, your unit test can remain lightning-fast and isolated by simply passing a "fake" database object:

// user-service.test.js
const mockDb = { query: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }) };
const service = new UserService(mockDb);

// Now you can test UserService logic without a real database!

By decoupling the creation of an object from its usage, you eliminate the need for complex global state manipulation or monkey-patching libraries during test setup. This pattern ensures that your unit tests focus strictly on the logic of the component itself, rather than the behavior of the external systems it relies upon.

Key Takeaway: By injecting dependencies via constructors rather than importing them directly, you transform opaque, tightly coupled code into modular, testable units that can be verified in total isolation.