Using mocking to test an API service in Salesforce

Mocking is a powerful addition to any Salesforce developer's arsenal.

It speeds up the creation of test scenarios, allowing you to avoid the usual heavyweight approaches involving inserting heaps of test data.

It also helps isolate code from dependencies such as databases or apis. This further simplifies the setup and allows all the focus to remain on the unit under test.

This post builds up a real-world Apex method over several iterations, and demonstrates how to use the ApexMocks mocking library to test each step.

Table of contents

What are we testing?

The unit under test is an @AuraEnabled method called GetFXRate(). It's part of a class called LWCService and is intended to be called by an LWC component to get currency exchange rate data.

The first version of GetFXRate(), shown below, only implements the very basics. There's no exception handling, logging, or retrying failed api requests yet. These will be added later. To begin with it simply makes a call to get data, assumes this request is successful, and returns a response to the LWC component.

public class LWCService {
    
    /**
     * The class has just a single dependency for
     * the moment... a service that calls an API
     * to get exchange rate data.
     */
    @testVisible
    private static FXService fxService;
    
    static {
    	fxService = new FXService();
    }
    
    /**
     * Inner class defining the shape of the
     * response expected by the LWC component.
     */
    public class Response() {
    	public Boolean isSuccess {get;set;}
        public String message {get;set;}
        public Object data {get;set}
        
        public Response() {
            this.isSuccess = false;
        }
    }
    
    /**
     * Method that will call the Foreign Exchange service
     * to get the latest exchange rate between 2 currencies.
     */
    @AuraEnabled
    public static Response GetFXRate(String fromCurr, String toCurr) {
    
        Response response = new Response();
    	
        /**
         * This code assumes that the request to the FXService
         * will always be a success.
         * 
         * We will improve it by adding exception handling,
         * a retry mechanism, and logging as we go along.
         */
        Decimal exchangeRate = fxService.getRate(fromCurr, toCurr);
        response.isSuccess = true;
        response.data = exchangeRate;
        
        return response;
    }
}

What needs to be tested?

Not the dependencies. At least not in a unit test.

Unit tests should be quick to write, easy to set up, and laser-focused on the code being tested.

As you can see, the LWCService has just a single dependency, the FXService (foreign exchange service), which is called to get exchange rate data.

The FXService handles all of the low-level details of interacting with an FX api.

It deals with authentication, makes HTTP requests, and processes HTTP responses. It either returns a Decimal representing the exchange rate, or if there is an issue it will throw some type of Exception.

None of this is the responsibility of the LWCService, however, which just wants to get the data without worrying about how it's done. Crucially, we don't want to waste time retesting code that should already have its own suite of unit tests.

This is where the technique of mocking can save a developer lots of time.

With the help of the ApexMocks library, a developer can set up stubbed (i.e. fake) responses for dependencies of the code they are testing.

There's no need to insert data into the database, or create large numbers of static resource files to represent api responses. Even complex test scenarios can be set up quickly and easily.

ApexMocks: Testing the happy path

Onto the first test, which can be expressed as follows...

"Given that the FXService returns an exchange rate, then a success response should be returned to the UI."

Setting up a stubbed response with ApexMocks

We will use ApexMocks to preprogram the FXService's behaviour in just a few lines of code, and don't have to implement HttpCalloutMock or MultiStaticResourceCalloutMock to mock the api response itself.

I go into the finer details of setting up stubbed (fake) responses in another post, but the basic steps are:

  1. Create a fake version of your dependency. This is called a mock object.
  2. Control how the mock object behaves during the test by preprogramming (stubbing) its responses.
  3. Inject the fake dependency into your unit under test.

In the example unit test below, I use the thenReturn() stubbing method to preprogram the FXService to return an exchange rate of 1.5 (when called with GBP and USD arguments).

The mockFX mock object is then assigned to the private, but @testVisible, fxService property which means that it will be used during the test instead of the real version.

@isTest
private static void GetRate_OnAPISuccess_ReturnSuccessResponseToUI() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    /**
     * Create a mock object of the type FXService.
     */
    FXService mockFX = (FXService)mocks.mock(FXService.class);
    
    /**
     * When the FXService.getRate() method is called with
     * 'GBP' and 'USD' arguments...
     * ...then return an exchange rate of 1.5.
     */
    mocks.startStubbing();
        mocks.when(mockFX.getRate('GBP', 'USD'))
            .thenReturn(1.5);
    mocks.stopStubbing();
    
    // Test
    /**
     * Inject the mock FXService into our unit under test
     * so it is used instead of the real dependency.
     */
    LWCService.fxService = mockFX;
    LWCService.Response response = LWCService.GetFXRate('GBP', 'USD');
    
    // Assert
    System.assert(response.isSuccess);
    System.assertEquals(1.5, response.data);
}

LWCService: Adding exception handling

To improve the GetFXRate() method, we now need to add exception handling.

If there is a problem with the FXService such as the underlying api going down, then we need to catch the thrown exception and return a helpful message to let the user know there's an issue.

The updated method below shows the call to the FXService is now wrapped in a try-catch statement. Any exceptions will result in an error response along with a user-readable message.

@AuraEnabled
public static Response GetFXRate(String fromCurr, String toCurr) {

    Response response = new Response();

    try {
        Decimal exchangeRate = fxService.getRate(fromCurr, toCurr);
        response.isSuccess = true;
        response.data = exchangeRate;
    } catch(Exception ex) {
        response.isSuccess = false;
        /**
         * The end user doesn't want to see an ugly stack trace, so
         * we send back a more helpful, non-technical message.
         */
        response.message = Label.UserReadableMessage;
    }
        
    return response;
}

ApexMocks: Testing exception handling

The next challenge, then, is to test the exception handling...

"Given that the FXService throws an exception, then an error response should be returned to the UI."

The code is virtually identical to the first test, except that we use a different ApexMocks stubbing method called thenThrow().

The example below shows the FXService stubbed to throw a ServerError500Error, and assertions confirming the shape and content of the error response.

@isTest
private static void GetRate_OnAPIError_SendErrorResponseToUI() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    FXService mockFX = (FXService)mocks.mock(FXService.class);
    
    mocks.startStubbing();
        mocks.when(mockFX.getRate('GBP', 'USD'))
            .thenThrow(new new ServerError500Exception(
                'Something bad happened'
            ));
    mocks.stopStubbing();
    
    // Test
    LWCService fxService = mockFX;
    LWCService.Response response = LWCService.GetFXRate('GBP', 'USD');
    
    // Assert
    System.assertEquals(false, response.isSuccess);
    System.assertEquals('We would politely like to inform you that our FX api has gone to s%@t', response.message);
}

LWCService: Adding a retry mechanism

To make the GetFXRate() method even more robust, we're going to add a retry mechanism. This way, if the FXService's underlying api goes down temporarily or is swamped by traffic, retrying still gives us a chance to succeed.

As you can see below, a maximum of 3 requests for data will be made. If any of the requests succeed, a success response with the exchange rate will be returned to the user. On the other hand, if all 3 requests fail, we will return the same error response as in the previous section.

@AuraEnabled
public static Response GetFXRate(String fromCurr, String toCurr) {

    Response response = new Response();
    
    /**
     * A basic retry mechanism has been added,
     * to make the method more robust. The api
     * will be tried up to 3 times before it
     * returns a failure message.
     */
    Integer maxRetries = 3;
    Integer retryCount = 0;
    while(!response.isSuccess && retryCount < maxRetries) {
        try {
            Decimal exchangeRate = fxService.getRate(fromCurr, toCurr);
            response.isSuccess = true;
            response.data = exchangeRate;
        } catch(Exception ex) {
            retryCount++;
            response.isSuccess = false;
            response.message = Label.UserReadableMessage;
        }
    }
        
    return response;
}

ApexMocks: Testing the retry mechanism

The first retry scenario to test is where all 3 consecutive requests to the FXService fail...

"Given that the FXService is down, then a maximum of 3 requests for data should be made before an error response is returned to the UI."

We can set up the FXService's stubbed response using the thenThrow() method again. It will continue to throw the same exception no matter how many times it is called, which replicates how it will behave when the api is down for a long period of time.

So, we want to check that retry mechanism is working, but there's one problem... there's no way to tell this just by looking at the error response. This will be the same whether there was just 1 request or 20. How can we be sure that exactly 3 attempts were made to get data from the FXService?

This is where the ApexMocks verify() method comes in useful.

During a test, ApexMocks counts all calls to mock object methods, and records the arguments that they were called with. You can then use the verify() method to query this data and validate the internal behaviour of your unit under test.

To test our retry scenario, we will use verify() to confirm that the (mock) FXService.getRate() method was called 3 times with 'GBP' and 'USD' arguments.

@isTest
private static void GetRate_MultipleAPIErrors_RequestsAreRetried() {
    
    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    FXService mockFX = (FXService)mocks.mock(FXService.class);
    
    mocks.startStubbing();
    	// A 500 error will continue to be thrown,
        // simulating a scenario where the api is down.
        mocks.when(mockFX.getRate('GBP', 'USD'))
            .thenThrow(new ServerError500Exception('Sad face'));
    mocks.stopStubbing();
    
    // Test
    LWCService fxService = mockFX;
    LWCService.Response response = LWCService.GetFXRate('GBP', 'USD');
    
    // Assert
    System.assertEquals(false, response.isSuccess);
    System.assertEquals('We would politely like to inform you that our FX api has gone to s%@t', response.message);
    /**
     * Using verify() allows us to confirm that the retry
     * mechanism is working as expected. This couldn't be
     * done by just checking the response.
     *
     * The following statement verifies that the 
     * FXServices's getRate() method was call 3 times
     * with 'GBP' and 'USD' arguments.
     */
    ((FXService)mocks.verify(mockFX, 3)).getRate('GBP', 'USD');
}

ApexMocks: Chaining stubbing methods to test a complex retry scenario

The second retry scenario to test is slightly more complex - the underlying api fails on the first couple of attempts, but on the final retry it is up again so the request succeeds.

"Given that the FXService fails less than the maximum number of retries, then a success response should be returned to the UI."

Fortunately this is easy to do with ApexMocks by chaining stubbing methods together.

In the following test, thenThrowMulti() is used to set up exception responses for the first 2 requests (the original request and the first retry), then the thenReturn() method is chained to configure a success response on the third try.

@isTest
private static void GetRate_SuccessAfterErrors_RequestsAreRetried() {
  
    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    FXService mockFX = (FXService)mocks.mock(FXService.class);

    /**
     * Multiple stubbing methods are chained
     * together to simulate a scenario where
     * the api request fails twice before
     * it eventually succeeds.
     */
    mocks.startStubbing();
        mocks.when(mockFX.getRate('GBP', 'USD'))
        	// the api is down temporarily...
            .thenThrowMulti(new List<Exception>{
                new ServerError500Exception('Argh!'),
                new ServerError500Exception('Yikes!')
            })
            // ...but is up again on the third attempt
            .thenReturn(1.5);
    mocks.stopStubbing();

    // Test
    LWCService.FXService = mockFX;
    LWCService.Response response = LWCService.GetFXRate('GBP', 'USD');

    // Assert
    /**
     * The retry mechanism is working correctly as
     * a success response is returned.
     */
    System.assertEquals(true, response.isSuccess);
    System.assertEquals(1.5, response.data);
    /**
     * Just double-checking that the api was called
     * 3 times.
     */
    ((FXService)mocks.verify(mockFX, 3)).getRate('GBP', 'USD');
}

LWCService: Adding logging and notifications

To finish off the GetFXRate() method it would be good to ensure that any errors are logged to help with debugging issues, and that admins will get notified if the api is down.

This means adding two more dependencies to the LWCService, Logger and Emailer.

public with sharing class LWCService {
    
    @testVisible
    private static FXService fxService;
    /**
     * Additional dependencies that will be swapped
     * for mock objects during tests.
     */
    @testVisible
    private static Logger logger;
    @testVisible
    private static Emailer emailer;

    static {
        fxService = new FXService();
        logger = new Logger();
        emailer = new Emailer();
    }

    public class Response {
        public Boolean isSuccess {get;set;}
        public String message {get;set;}
        public Object data {get;set;}
    }

    @AuraEnabled
    public static Response GetFXRate(String fromCurr, String toCurr) {

        Response response = new Response();

        Integer maxRetries = 3;
        Integer retryCount = 0;
        while(!response.isSuccess && retryCount < maxRetries) {
            try {
                Decimal rate = fxService.getRate(fromCurr, toCurr);
                response.isSuccess = true;
                response.data = rate;
            } catch(Exception ex) {
                retryCount++;
                response.isSuccess = false;
                response.message = Label.UserReadableMessage;

                /**
                 * The production logger is likely to call an
                 * api or create a platform event. Testing
                 * can be simplified by swapping in a mock-object
                 * and simply verifying that the method gets called
                 * when it should.
                 */
                logger.log(ex.getMessage());
            }
        }

        if(!response.isSuccess) {
            /**
             * Will use the verify() method to
             * check that this is called if all
             * retries have failed.
             */
            emailer.notifyAdmins();
        }

        return response;
    }
}

ApexMocks: Testing logging and notifications

Logging and notifications are, again, not the responsibility of the LWCService, and so have been implemented in separate classes. We're not interested in (re)testing the internals of our dependencies, but rather we're just focusing on verifying the logic in our unit under test.

"Given that all requests to the FXService have failed, then each error message should be logged and admins should be sent a notification that the api is down."

The test below creates mock objects for the new dependencies so that ApexMocks will record all their interactions. At the end of the test, verify() is used again to confirm the underlying behaviour.

@isTest
private static void GetRate_AllRetriesFail_VerifyLogsAndEmailSent() {
  
    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    FXService mockFX = (FXService)mocks.mock(FXService.class);
    /**
     * Mock-objects are created for the new dependencies.
     */
    Logger mockLogger = (Logger)mocks.mock(Logger.class);
    Emailer mockEmailer = (Emailer)mocks.mock(Emailer.class);

    /**
     * As logger.log() and emailer.notifyAdmins() are void
     * methods, we don't want to (and can't) create a stub
     * response. There would be nothing to return. We simply
     * want to verify that the methods are called.
     */
    mocks.startStubbing();
        mocks.when(mockFX.getRate('GBP', 'USD'))
            .thenThrow(new RateLimit429Exception('Too many requests'));
    mocks.stopStubbing();

    // Test
    LWCService.fxService = mockFX;
    /**
     * Inject the mock objects that will record the number
     * of times they are called.
     */
    LWCService.logger = mockLogger;
    LWCService.emailer = mockEmailer;
    LWCService.Response response = LWCService.GetFXRate('GBP', 'USD');

    // Assert
    ((Logger)mocks.verify(mockLogger, 3)).log('Too many requests');
    ((Emailer)mocks.verify(mockEmailer, 1)).notifyAdmins();
}

Conclusion

I hope you found that useful. Please check out some of my other posts that go into more details about the mechanics of using ApexMocks.

💡
All of the code examples in this post are available from this GitHub repo.