Dependency Injection and Apex Mocks for Faster Tests

Dependency Injection and Apex Mocks for Faster Tests
Photo by Fernando Rodrigues / Unsplash

If you're using the fflib library you're probably well aware of it's strengths as an enterprise application design framework. Using some form of Dependency Injection in conjunction with Apex Mocks is an approach that really helps you capitalize on these strengths and write more efficient tests.

I thought I would share my current implementation of this strategy using a simple example to demonstrate how effective this approach can be in dynamically injecting dependencies, and isolating logic in your tests.

Let's say we have a user interface where individuals can submit a pet adoption application that includes their name and the type of pet they would like to adopt. The UI passes the application to our controller, which then retrieves all adoptable pets from Shelters and Foster Homes in our database via an adoption application service, returning a list of pets that match the user's type preference.

Thinking about this structure using a Separation of Concerns approach, we have a Presentation Layer handling the user input, and a Service Layer executing business logic and accessing our database via Selectors.

Our Simple Pet Adoption App

Without dependency injection, our controller depends on the service, which depends on the selectors. If we run a test on the controller we're also running the code in all of its dependent classes. This means we have to concern ourselves with what those classes are doing when all we want to do is test the controller.

Additionally, these dependencies prevent us from having truly separate layers, and they slow our unit tests down because we are required to perform DML in order for the selectors have data to access.

There are a number of strategies for utilizing dependency injection, but I've found the use of an Injector class to be quite useful and intuitive. I've included an additional property that I use to indicate if we want a "stub" instance of a class returned, which is useful as we'll see later in the testing portion.

public class Injector {

    public static Boolean stubbing = false;

    public static Object instantiate(String className) {
        if (stubbing) {
            String stubClassName = MockFactory.getStubClassName(className);
            return MockFactory.getMember(stubClassName);
        } else {
            Type t = Type.forName(className);
            return t.newInstance();
        }
    }
    
}
Injector.cls

Now whenever we instantiate a class we use the Injector and cast the returned Object to the type we need. For example, let's look at how the AdoptionApplicationController accesses the AdoptionApplicationService.

public without sharing class AdoptionApplicationController {

    private class NoPetsAvailableException extends Exception {}
    
    public static List<PetDetail> getAdoptablePets(AdoptionApplication application) {
        List<PetDetail> pets = new List<PetDetail>();
        
        AdoptionApplicationService service = (AdoptionApplicationService) Injector.instantiate('AdoptionApplicationService_Impl');

        // Get foster homes with adoptable pets
        List<FosterHome> fosterHomes = service.getFosterHomesWithAdoptablePets(new List<AdoptionApplication>{application});
        for (FosterHome fosterHome : fosterHomes) {
            for (Pet pet : fosterHome.pets) {
                if (pet.species == application.species) {
                    pets.add(new PetDetail(pet, 'Foster Home'));
                }
            }
        }

        // Get shelters with adoptable pets
        List<Shelter> shelters = service.getSheltersWithAdoptablePets(new List<AdoptionApplication>{application});
        for (Shelter shelter : shelters) {
            for (Pet pet : shelter.adoptablePets) {
                if (pet.species == application.species) {
                    pets.add(new PetDetail(pet, 'Shelter'));
                }
            }
        }

        if (pets.isEmpty()) {
            throw new NoPetsAvailableException('No pets available for adoption');
        }

        return pets;
    }

    public class PetDetail {
        public Pet pet;
        public String location;

        public PetDetail(Pet pet, String location) {
            this.pet = pet;
            this.location = location;
        }
    }

}
AdoptionApplicationController.cls

The AdoptionApplicationService is an interface (a shell containing method signatures) which can have multiple implementations (classes that implement the interface and actually define the methods).

I've created a simple default implementation called AdoptionApplicationService_Impl, but we could create a range of versions for different purposes as needs arise.

On line 8 you can see that we tell the Injector to give us an instance of the AdoptionApplicationService and pass in a String representing the class name of the implementation we need. Here I've hardcoded AdoptionApplicationService_Impl, but we could dynamically instantiate implementations by storing their names in metadata.

Great, now we've achieved the first step of decoupling our controller's logic from the service it utilizes. The controller knows there is a service (defined by the interface), but does not have to be concerned with the specific logic contained within.

On to our tests. We can now use the Apex Mocks library in conjunction with Salesforce's Stub API to mock dependencies and isolate our test so it covers only the controller's logic. As recommended, I'm using a MockFactory to facilitate the generation of stubs, but I included a few helpful properties and methods that allow us to work with the Apex Mocks library.

public without sharing class MockFactory {

    public static fflib_ApexMocks mocks = new fflib_ApexMocks();
    public static Map<String,Object> factoryMembers = new Map<String,Object>();

    public static String getStubClassName(Object mockInstance) {
        return fflib_ApexMocks.extractTypeName(mockInstance);
    }

    public static String getStubClassName(String className) {
        return className + '__sfdc_ApexStub';
    }

    public static void addMember(Object mockInstance) {
        factoryMembers.put(getStubClassName(mockInstance), mockInstance);
    }

    public static Object getMember(String stubClassName) {
        return factoryMembers.get(stubClassName);
    }

    public static Object generateStub(String className) {
        Object mockInstance = mocks.mock(Type.forName(className));
        addMember(mockInstance);
        return mockInstance;
    }

}
MockFactory.cls

The MockFactory helps us create, store, and access the mocks generated during our test, which is best illustrated by building a test method:

@isTest
private static void testGetAdoptablePetsWithResults() {
    // Setup
    AdoptionApplication application = new AdoptionApplication(
    'TestApplicant',
        'Dog'
    );

    // Tell Injector to stub instantiated classes during test
    Injector.stubbing = true;

    // Define mock objects
    fflib_ApexMocks mocks = MockFactory.mocks;
    AdoptionApplicationService mockService = (AdoptionApplicationService) MockFactory.generateStub('AdoptionApplicationService_Impl');

    mocks.startStubbing();
    mocks.when(mockService.getFosterHomesWithAdoptablePets(new List<AdoptionApplication>{application}))
        .thenReturn(new List<FosterHome>{getTestFosterHome('Dog')});
    mocks.when(mockService.getSheltersWithAdoptablePets(new List<AdoptionApplication>{application}))
        .thenReturn(new List<Shelter>());
    mocks.stopStubbing();

    // Run the test
    List<AdoptionApplicationController.PetDetail> pets = AdoptionApplicationController.getAdoptablePets(application);

    // Verify results
    System.assertEquals(1, pets.size(), 'Verify one available pet is returned');
    System.assertEquals('Dog', pets[0].pet.species, 'Verify pet species is correct');
}

In order to create a mocked version of the AdoptionApplicationService we call MockFactory.generateStub() and pass in the name of a class implementing the service interface.

We need to define each method within the service because they are going to be called from the controller. To keep it simple I'm just returning a single Foster Home with a dog available for adoption, and an empty list of Shelters. For our test we're using an application seeking a dog to adopt, so when we pass that application in to the controller we can verify that we get our mocked response with one available dog.

Since the AdoptionApplicationController instantiates the AdoptionApplicationService through the Injector, and stubbing was set to true in our test, the Injector returns the Stub API instance of the service from the MockFactory. This allows us to control what we get back from the service methods and ensure that the controller behaves how we expect in the test, without the dependencies.

We can repeat this pattern in tests designed to cover the AdoptionApplicationService by mocking the Selectors. Without injection we'd have multiple test classes all requiring DML operations to run, but now we only need to actually insert and retrieve records when testing the selector layer. Utilizing this pattern across an ever-increasing number of classes will help keep our test suites from taking forever to run.