Skip to main content

Dependency Injection in Salesforce Apex: Testability & Trade-offs

๐Ÿ’ฌ In plain words:

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 the StubProvider interface, 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).
๐Ÿ“Œ Example:

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.StubProvider to generate responses dynamically on the fly.
๐Ÿง  The Golden Rule of DI: Inject the tool so tests can hand in a fake. The price is slightly more classes (interfaces). The prize is fast, honest, and unbreakable tests.

๐Ÿงญ 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: StubProvider only 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.
⚠️ Developer Trap: Static Methods & Unstubbable Code

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

Q: What is the difference between testing a service with real data versus an injected stub? When should I use each?
๐ŸŽฏ Say this first: Real-data tests prove integration (you need a few of these, and they are slow). Stub-injected tests prove logic (you need many of these, and they are fast).

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

Q: What are the practical limitations of the Salesforce StubProvider?

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 Schema or PageReference)
  • 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.

Q: My team argues that Dependency Injection is "over-engineering" for simple Salesforce projects. How should I respond?

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.

Q: Does Dependency Injection impact Salesforce Governor Limits?

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.