principleModeratepending
Principle: Dependency inversion for testable code
Viewed 0 times
dependency inversiondependency injectionsolidtestabilityloose coupling
Problem
Code that directly instantiates its dependencies is hard to test, hard to change, and tightly coupled.
Solution
Depend on abstractions, inject implementations:
The principle (SOLID 'D'):
Practical application:
Where to apply:
Where NOT to apply:
## Tightly coupled (hard to test):
class OrderService {
process(order) {
const db = new PostgresDB(); // Hard-coded dependency
const emailer = new SendGridClient(); // Hard-coded dependency
db.save(order);
emailer.send(order.userEmail, 'Order confirmed');
}
}
// To test: need real DB and email service!
## Dependency inversion (easy to test):
class OrderService {
constructor(db, emailer) { // Dependencies injected
this.db = db;
this.emailer = emailer;
}
process(order) {
this.db.save(order);
this.emailer.send(order.userEmail, 'Order confirmed');
}
}
// Production:
new OrderService(new PostgresDB(), new SendGridClient());
// Testing:
new OrderService(mockDB, mockEmailer); // No real services!The principle (SOLID 'D'):
- High-level modules should not depend on low-level modules
- Both should depend on abstractions
- Abstractions should not depend on details
Practical application:
- Accept dependencies through constructor/function parameters
- Define interfaces for dependencies (what, not how)
- Wire up real implementations at the top level (composition root)
- In tests, pass in fakes/mocks/stubs
Where to apply:
- Database access
- External API clients
- File system operations
- Time/clock (for deterministic tests)
- Random number generation
Where NOT to apply:
- Standard library functions
- Pure utility functions
- Value objects and data classes
Why
Injecting dependencies creates seams in the code where you can substitute implementations. This makes testing easy, changes cheap, and coupling visible.
Context
Software architecture and testing
Revisions (0)
No revisions yet.