Dependency Injection (DI) simply means a class receives its tools from the outside instead of building them internally. By passing dependencies (like database selectors or API callout classes) into a class, you can easily substitute them with FAKE tools (mocks) during unit tests. This lets you test your core business logic instantly without making real database queries or external network callouts.
Key Points
- Decoupling: Classes depend on interfaces (abstractions) rather than hard-coded class implementations.
- Speed & Isolation: By injecting dependencies, tests run in milliseconds because they bypass real DML operations and SOQL queries.
- Native Mocking: Salesforce provides
Test.createStub()and theStubProviderinterface, meaning you can dynamically mock classes without writing tedious manual fake classes. - Strategic Use: Full DI frameworks can be overkill in Apex. Inject dependencies strategically at system seams (like API integrations and data selectors).
Imagine a
DiscountCalculator that instantiates its own ERPClient. Any unit test you write will automatically attempt a real network callout. Now, flip the architecture: The calculator receives an
IErpClient interface in its constructor. In production, you pass the real client. In tests, you pass a fake client that simply returns a flat $50.00. The logic is tested in 50 milliseconds with zero network dependency.
๐ฌ Real-Life Example: The Billing Service
Let's look at a BillingService that handles live transactions.
The Old Way (Hard-coded):
The service creates its gateway client inline: PaymentGatewayClient gateway = new PaymentGatewayClient();. Because the dependency is hard-wired inside the method, test classes cannot swap it out. Tests either skip the critical logic entirely or crash when hitting the callout limit.
The New Way (Dependency Injection):
- Define an interface:
IPaymentClient. - Receive it via the constructor (injected), rather than using
new. - In your test class, hand in a dynamic mock using
Test.createStub().
The Payoff:
Your pricing tests run entirely offline. They execute instantly and only fail if your actual business logic is broken, not because an external API went down.
How Dependency Injection Works in Apex
To implement DI effectively in Salesforce, you rely on two main concepts:
- Depend on Interfaces: Your class constructors should accept interfaces, not concrete implementations. This is usually handled via Constructor Injection.
- Use the Stub API: In your test contexts, you don't need to write manual mock classes. You use
System.StubProviderto generate responses dynamically on the fly.
๐งญ 360 Card: Dependency Injection & Testability
- The Rule: Depend on interfaces and receive the implementations from the outside.
- The Gain: Logic tests that run in milliseconds. No DML operations, no SOQL queries, and no callouts required. Your test suite stops being a deployment bottleneck.
- The Price: More classes, more files, and more architectural indirection. (Note: Using heavy Java-style DI containers in Apex is usually overkill).
- The Limits:
StubProvideronly works on non-final, non-static, visible methods. You cannot stub standard system classes, static methods, or hidden managed package code. - At Volume: An enterprise org relying purely on real-data testing (without mocking) will eventually see test suites take 90+ minutes, blocking critical releases.
The Salesforce
StubProvider cannot mock static methods. If your codebase is filled with static utility methods that perform DML or callouts, you cannot inject or mock them dynamically. Move away from static contexts for complex logic if you want true testability.
Core Q&A
Real-data tests: These use @testSetup to insert records into the database, invoke your service, and query to assert state changes. They are slow but necessary to validate the entire stack (Triggers, Flows, Validation Rules, and FLS).
Stubbed unit tests: These inject a fake database selector that returns mock in-memory sObjects. They run in milliseconds, perform no DML, and isolate your specific unit of code. If someone breaks a Validation Rule on the Account object, your Invoice logic test won't magically fail.
A healthy Salesforce org needs a test pyramid: a massive base of fast, stubbed logic tests, capped by a thin layer of end-to-end integration tests.
Implementation Pattern
// 1. Depend on an INTERFACE, not a concrete class.
public interface IInvoiceSelector {
List<Invoice__c> byIds(Set<Id> ids);
}
public class InvoiceService {
private IInvoiceSelector selector;
// 2. The Injection Point: Receive dependency via constructor
public InvoiceService(IInvoiceSelector s) {
this.selector = s;
}
// 3. Default Constructor: Wires the real class for production use
public InvoiceService() {
this(new InvoiceSelector());
}
}
// In your Test Class:
// You can now dynamically inject a stubbed selector!
InvoiceService service = new InvoiceService(
(IInvoiceSelector) Test.createStub(IInvoiceSelector.class, new MockSelectorProvider())
);
Advanced Scenarios
The StubProvider is powerful but strict. It only works on methods that are non-final, non-static, and visible to your code. You cannot mock:
- Static methods
- Standard Salesforce system classes (like
SchemaorPageReference) - Classes from managed packages (different namespaces)
The Fix: Wrap these unstubbable elements behind your own thin, instance-based interface classes right at the boundary of your architecture. Your internal code then depends on the wrapper, which is stubbable.
Agree on the scope, but hold firm on the principle.
- Building massive, reflection-based DI containers (like Java Spring) is overkill in Apex.
- However, lightweight constructor injection is not over-engineering. It's basic modular design.
- The compromise: Only inject at your architectural seams. Put external API callouts and complex SOQL selectors behind interfaces. Leave simple utility classes alone.
This approach gives you 80% of the testability benefits for 10% of the architectural overhead.
Barely. Passing an interface into a constructor uses a negligible amount of heap space and CPU time compared to static methods. Unless you are instantiating thousands of complex objects inside a deeply nested for loop (which is an anti-pattern anyway), the DI overhead will never trigger limit exceptions. The testing benefits massively outweigh the microscopic performance cost.