Apex Dependency Injection

This is how I setup and use dependency injection in Apex in 2025.

Apex Dependency Injection

Dependency Injection in Salesforce Apex is an odd fit at first - whether you’re coming from other languages where Dependency Injection is easy, or you’re an admin-veloper who somehow got lost and landed on this page. Follow along with this article to understand how to put Dependency Injection to work for you in Salesforce Apex.

Why DI?

  1. I want a pattern that makes it easier to write tests. If its easier to write tests, I will write more tests and I will write better tests. When I have strong tests, I will have confidence that future changes have not broken existing functionality.
  2. Test Execution Speed: tests that use DI instead of calling the database run way faster. We’re already going slow in Salesforce land because we have to push to the cloud to do anything. Don’t make yourself slower by hitting the database when you don’t have to. And again, faster tests are easier tests. I can run them more frequently. I will use them more.

Where did we come from?

Using dependency injection in Apex used to require doing things the hard way with Interfaces. Because it was so time consuming, I would limit the usage to wrapping platform APIs like DML, Email, SOQL/SOSL Queries, HttpRequests, etc. I’d write an interface to describe what I was about to do. I would write one implementation of that interface to complete the task, then I would create a second, mock implementation that my tests could use later.

What changed?

Spring ‘17 added the StubProvider Interface.

With the addition of the StubProvider Interface, the community has created a number of mocking frameworks. My favorite is Salesforce’s apex-mockery. This tool feels similar to Java’s mockito or Javascript’s jest.

How do I do it?

Write a class that has a dependency.

public with sharing class SetContactOwnerByAccount {
  // The dependency to be injected.
  private AccountRepo accRepo;

  private CollectionsUtil cutil = new CollectionsUtil();

  public SetContactOwnerByAccount() {
    // Usually it uses a regular AccountRepo.
    this(new AccountRepo());
  }

  public SetContactOwnerByAccount(AccountRepo accRepo) {
    // This constructor is how a test can "Inject" a mock.
    this.accRepo = accRepo;
  }

  public void execute(List<Contact> contacts) {
    Set<Id> accountIds = cutil.toSetOfId(contacts, 'AccountId');
    Map<Id, Account> accountsById = new Map<Id, Account>(
      // Here the dependency is used to query accounts by id.
      // We'll have to mock this in a test.
      accRepo.queryByIds(accountIds)
    );

    for (Contact con : contacts) {
      Id accountId = con.AccountId;

      // I can hit this path by passing in a Contact with no AccountId.
      // I don't need Dependency Injection to test this.
      if (String.isBlank(accountId)) {
        continue;
      }

      // I can hit this path by passing in a Contact with an AccountId
      // that doesn't exist.
      // Dependency Injection makes this trivial.
      if (!accountsById.containsKey(accountId)) {
        continue;
      }

      // This is the happy path where I have a Contact with an AccountId
      // that exists.
      // Dependency Injection makes this much easier to test because I
      // am creating the Account and the Contact together in my test.
      Account parent = accountsById.get(accountId);
      con.OwnerId = parent.OwnerId;
    }
  }
}

Write a test for your class and mock the dependency.

@IsTest
private class SetContactOwnerByAccounTest {
  private static final Id ACCOUNT_ID;
  private static final Id OWNER_ID;

  // Define the Mock and the Spy that I'll be using.
  static Mock accRepo;
  static MethodSpy ar_queryByIds;

  // This is the class that I'm testing.
  static SetContactOwnerByAccount sut;

  static {
    // Initialize these constants so we can assert against them later.
    ACCOUNT_ID = MockIdGenerator.getMockId(Account.getSObjectType());
    OWNER_ID = MockIdGenerator.getMockId(User.getSObjectType());

    // The fake Account that will be returned by the mocked AccountRepo.
    Account acc = TestUtil.createAccount('TestAccount');
    acc.Id = ACCOUNT_ID;
    acc.OwnerId = OWNER_ID;

    // Using apex-mockery to initialize the Mock and Spy.
    accRepo = Mock.forType(AccountRepo.class);
    // Perhaps the greatest weakness is requiring a String for the method name.
    ar_queryByIds = accRepo.spyOn('queryByIds');
    // Telling the Mock exactly what we want it to return.
    ar_queryByIds.returns(new List<Account> { acc });

    // Injecting the Mock to the System Under Test.
    sut = new SetContactOwnerByAccount((AccountRepo) accRepo.stub);
  }

  @IsTest
  static void it_should_NOT_set_ownerid_when_contact_does_NOT_have_accountid() {
    Contact noAccount = TestUtil.createContact('NoAccount');

    Test.startTest();

    sut.execute(new List<Contact> { noAccount });

    Test.stopTest();

    // Using apex-mockery to Expect that my mock method was called exactly once.
    Expect.that(ar_queryByIds).hasBeenCalledTimes(1);
    // Using apex-mockery to Expect that my mock method was called
    // with a specific argument.
    Expect.that(ar_queryByIds).hasBeenCalledWith(new Set<Id> { null });

    // Using standard apex asserts only after I have passed
    // the apex-mockery Expects.
    // If my methods aren't called the way I expect, then these Asserts
    // on the data are worthless.
    System.Assert.isNull(
      noAccount.OwnerId,
      'OwnerId should be null because there is no AccountId.'
    );
  }

  @IsTest
  static void it_should_NOT_set_ownerid_when_contact_accountid_is_not_found() {
    Contact accountNotFound = TestUtil.createContact('accountNotFound');
    accountNotFound.AccountId = MockIdGenerator.getMockId(Account.getSObjectType());

    Test.startTest();

    sut.execute(new List<Contact> { accountNotFound });

    Test.stopTest();

    Expect.that(ar_queryByIds).hasBeenCalledTimes(1);
    Expect.that(ar_queryByIds).hasBeenCalledWith(new Set<Id> { accountNotFound.AccountId });

    System.Assert.isNull(
      accountNotFound.OwnerId,
      'OwnerId should be null because there is no Account found using the Contact\'s AccountId.'
    );
  }

  @IsTest
  static void it_should_set_ownerid_when_contact_has_accountid() {
    Contact withAccount = TestUtil.createContact('WithAccount');
    withAccount.AccountId = ACCOUNT_ID;

    Test.startTest();

    sut.execute(new List<Contact> { withAccount });

    Test.stopTest();

    Expect.that(ar_queryByIds).hasBeenCalledTimes(1);
    Expect.that(ar_queryByIds).hasBeenCalledWith(new Set<Id> { ACCOUNT_ID });

    System.Assert.areEqual(OWNER_ID, withAccount.OwnerId);
  }
}

Sources

  1. Build your own Apex Mocking Framework
  2. StubProvider Interface
  3. apex-mockery
  4. Example Source
Built with Hugo
Theme Stack designed by Jimmy