Dependency Injection & Testability

Code that creates its own dependencies (new HttpService(), inline SOQL) is hard to test in isolation.

36.1 Why DI?

Code that creates its own dependencies (new HttpService(), inline SOQL) is hard to test in isolation. Dependency injection passes dependencies in, so tests can substitute stubs (§26.2).

36.2 Constructor injection

public interface IPaymentGateway {
    String charge(Id invoiceId, Decimal amount);
}

public with sharing class InvoiceService {
    private final IPaymentGateway gateway;
    public InvoiceService() { this(new StripeGateway()); }           // production default
    @TestVisible private InvoiceService(IPaymentGateway gateway) { this.gateway = gateway; }

    public String pay(Invoice__c inv) {
        return gateway.charge(inv.Id, inv.Amount__c);
    }
}

@isTest
private class InvoiceServiceTest {
    private class FakeGateway implements IPaymentGateway {
        public String charge(Id invoiceId, Decimal amount) { return 'txn_test_123'; }
    }
    @isTest
    static void paysThroughGateway() {
        InvoiceService svc = new InvoiceService(new FakeGateway());
        Assert.areEqual('txn_test_123', svc.pay(new Invoice__c(Amount__c = 10)));
    }
}

36.3 Metadata-driven DI

Other options: @TestVisible static fields holding dependencies (simple), the Force-DI open-source library, and fflib's Application factories with setMock.

public class Injector {
    private static Map<String, Object> overrides = new Map<String, Object>();

    public static Object instanceOf(String interfaceName) {
        if (overrides.containsKey(interfaceName)) return overrides.get(interfaceName);
        DI_Binding__mdt binding = DI_Binding__mdt.getInstance(interfaceName.replace('.', '_'));
        Type implType = Type.forName(binding.Implementation__c);
        return implType.newInstance();
    }

    @TestVisible
    private static void setMock(String interfaceName, Object mock) { overrides.put(interfaceName, mock); }
}

IPaymentGateway gw = (IPaymentGateway) Injector.instanceOf('IPaymentGateway');