Writing True Unit Tests using Dependency Injection and Mocking in Apex

In Salesforce by Alex0 Comments

Dependency Injection and Inversion of Control are all the rage these days even though the concept is very simple and has been around a long time, with frameworks such as Spring and Angular promoting this approach to application development.

Dependency Injection and Inversion of Control don’t require a framework to implement though, and you can get a lot of the benefits of both in Apex with a small amount of extra effort and planning when writing your code.

dependency-injection

Why use Dependency Injection?

The main advantages of dependency injection are:

  • decreased coupling between a class and its dependencies
  • making it easier to write different parts of an application independently by separating the interface from the implementation.
  • increased reusability, testability and maintainability by allowing clients to be developed that are not bound to a concrete implementation of their dependencies.
  • making it easier to write true, isolated unit tests using mock objects which simulate other parts of the application which are not currently under test.

Dependency Injection and Testing in Apex

The focus of this post is testing, in particular how the use of mock classes to allow you to build more robust unit tests. I’m not going to build a service locator, or a service factory, or show how the services to be injected can be controlled by configuration rather than code, although I may write about some or all of those things sometime in the future.

If you want to read more about those topics then I recommend reading this post by Jesse Altman and this post by Andrew Fawcett (part 2).

Setting Everything Up

The first thing that needs to be done to allow a service dependency to be injected is to define an interface for the service. The interface defines the contract that your service will adhere to, i.e. which methods it must implement and which arguments those methods take.

public interface IPieService {
    String getFlavour();
}

Once there is an the interface, you can define a simple implementation of it.

public class BasicPieService implements IPieService {
    public String getFlavour() {
        return 'Steak';
    }
}

The easiest form of dependency injection to understand and implement is constructor injection,. In simple terms this means that you pass all of your classes dependencies as constructor arguments. Here is a simple custom controller that uses constructor injection and calls the service interface.

public class PieController {
    private IPieService pieService;
    
    public PieController() {
        this(new BasicPieService());
    }
    
    public PieController(IPieService pieService) {
        this.pieService = pieService;
    }
    
    public String getFlavour() {
        return pieService.getFlavour();
    }
}

One thing you’ll notice here is that I’ve also provided a default constructor which always passes a BasicPieService into our class. This is because custom controllers must always have a no argument constructor available.

If I were to take this example a bit further and move away from constructor injection then I could replace this constructor with one that calls a service locator (or whatever mechanism of discovering services I decide put in place) to find and create instances of the services instead of hardcoding.

Time to Write some Tests

Since this is Salesforce, I suppose I should really write a unit test, otherwise how will I ever get this delicious class deployed to my live environment?

@isTest
private class PieControllerIntegrationTests {
    @isTest
    private static void getFlavour_ReturnsFlavour() {
        PieController controller = new PieController();
        
        Test.startTest();
            String flavour = controller.getFlavour();
        Test.stopTest();
        
        System.assertEquals('Steak', flavour);
    }
}

As you can see, this test class is fairly basic, but it covers off everything the controller does… or does it?

What is the true purpose of the getFlavour method? Is it to return ‘Steak’? Not quite. It’s purpose is to return the value provided by the getFlavour method of the IPieService that is injected in the constructor.

Does the test actually test the method’s true purpose? No.

Thinking about this a bit further, what happens if BasicPieService was changed? The test will fail.

Should the unit test of one class to fail when another class is changed? Ideally, no. Your unit tests should be isolated and only be impacted by changes to the class under test.

The test above is not really unit test, in fact it’s more of an integration test. This is no bad thing. Integration tests have their place and you need them in your application to ensure that everything works and plays nicely together, so this test class can stick around as it fulfils that purpose.

What would a Unit Test rather than an Integration Test look like for the Controller?

@isTest
private class PieControllerUnitTests {
    private class PieServiceMock implements IPieService {
        private String expectedFlavour;
        
        public PieServiceMock mockGetFlavour(String expectedFlavour) {
            this.expectedFlavour = expectedFlavour;
            return this;
        }
        
        public String getFlavour() {
            return expectedFlavour;
        }
    }
    
    @isTest
    private static void getFlavour_ReturnsFlavour() {
        PieServiceMock pieService = new PieServiceMock()
            .mockGetFlavour('Chicken & Mushroom');
            
        PieController controller = new PieController(pieService);
        
        Test.startTest();
            String flavour = controller.getFlavour();
        Test.stopTest();
        
        System.assertEquals('Chicken & Mushroom', flavour);
    }
}

The big difference between this test class and the previous one is that it is injecting a mock implementation of the IPieService into the constructor of the controller rather than relying on the behaviour of the BasicPieService.

The mock implementation has two methods. The mockGetFlavour method, which allows the mock to be setup and the return value for the getFlavour method to be set. The getFlavour method is required to fulfil the requirements of the interface, all it does is return the value that was passed into mockGetFlavour.

By using a mock service within the test class we have full control over the behaviour of the PieController as we can control exactly what the IPieService it uses returns in any given situation.

What are the advantages of this approach?

Imagine instead of having a hardcoded pie flavour (steak is delicious, but do you want it all the time?), what if it was instead defined in a Custom Setting? A new IPieService implementation would be required.

public class AdvancedPieService {
    public String getFlavour() {
        return Pie_Settings__c.getInstance().Flavour__c;
    }
}

The controller also needs to be updated to use this new service.

public class PieController {
    private IPieService pieService;
    
    public PieController() {
        this(new AdvancedPieService());
    }
    
    public PieController(IPieService pieService) {
        this.pieService = pieService;
    }
    
    public String getFlavour() {
        return pieService.getFlavour();
    }
}

How does this affect the test classes?

PieControllerUnitTests still passes! Hurrah! This is exactly what you want, as other than injecting a different dependency in the default constructor the class hasn’t changed, so the tests shouldn’t fail.

The same can’t be said for PieControllerIntegrationTests however, but this is a good thing. The integration test is meant to test how the different parts of the application interact and work together to produce a final result, and as one of the parts works has been fundamentally changed (and the final result altered) the test should fail.

It’s nice and simple to fix though.

@isTest
private class PieControllerIntegrationTests {
    @isTest
    private static void getFlavour_ReturnsFlavour() {
        insert new Pie_Settings__c(Flavour = 'Steak & Stilton');
        
        PieController controller = new PieController();
        
        Test.startTest();
            String flavour = controller.getFlavour();
        Test.stopTest();
        
        System.assertEquals('Steak & Stilton', flavour);
    }
}

What is the advantage of this approach?

Now that there is a true unit test in place for the PieController (and the other parts of the application, although I’ve omitted these from this post) it makes it much easier to find and identify problems.

For example, if the getFlavour method in the AdvancedPieService were changed to return null, only the AdvancedPieServiceUnitTests and PieControllerIntegrationTests (and any other integration tests that depend on the AdvancedPieService) should fail.

Immediately this helps identify that the AdvancedPieService is the problem, rather than any other class. If there were only integration-style tests available then it might not be immediately obvious that the AdvancedPieService is the root cause of the issue as you would have test failures all over the place.

Taking it further

I already touched upon how you can take dependency injection and inversion of control further by using service factories, service locators and using declarative data to drive which services are injected earlier in this post. There are tons of resources and blog posts out there that talk about dependency injection, although the best starting point you want more detail is probably this article by Martin Fowler.

To stick to more Salesforce specific issues. You can even start to take mocking a step further and use it to write SOQL and DML-less tests. These are particularly useful as they help protect your unit tests against declarative changes – for example, making a field required – that would cause integration-style tests to fail. They also have a few other advantages, including taking a lot less time to run than traditional Salesforce tests. That’s a topic for another post though.

I’d also suggest checking out ApexMocks if you’re serious about using mocks in your Apex unit tests as it will make your life a lot easier.

Whilst the approach presented in this post is very simple and only scratches the surface of what is possible using dependency injection, hopefully it has shown you how you can write more maintainable, easier to test code on Salesforce with only a little extra effort and thought up-front.