ApexMocks Documentation - Stubbing

This post contains documentation and examples for the stubbing methods available in the ApexMocks library.

What is stubbing?

When unit testing, you can replace your code's real dependencies with fake versions called mock objects.

The mock object methods can then be preprogrammed so that all of your dependencies behave in 100% predictable ways. This is a fast and flexible way to set up test scenarios that doesn't require inserting test data, meaning you spend more time focused on the unit under test.

The process of setting up these "canned" responses is called stubbing.

Setting up a stubbed response - step by step

  1. Create an instance of fflib_ApexMocks.
fflib_ApexMocks mocks = new fflib_ApexMocks();

2. Create a "mock object" for your dependency.

ApiService mockService = (ApiService)mocks.mock(ApiService.class);

At this stage, the mockService has all of the public instance methods that the ApiService has, but as no responses have yet been configured, they will all return null.

3. Let ApexMocks know you're about to start stubbing.

mocks.startStubbing();

4. Use the when() method, and one of the stubbing methods to set up a preprogrammed response for your mock object.

// When the mock object's getExchangeRate() method is called...
// ...with 'GBP' and 'USD' arguments *specifically*...
// ... then return 1.3.
mocks.when(mockService.getExchangeRate('GBP', 'USD'))
    .thenReturn(1.3);

5. Let ApexMocks know you've finished stubbing.

mocks.stopStubbing();

6. Inject the mock object into the class of the code you are testing. It will then be used during the test instead of the real version.

Where a static dependency is being mocked, assign the mock object to the @testVisible private static variable.

//... class of unit under test
public class LWCService {
 
    // The real dependency can be replaced during
    // tests with a mock object.
    @testVisible
    private static ApiService apiService = new ApiService();
     
    public static Response MethodBeingTested() {
        ...
    }
}
 
//...Swapping in the mock object during a test.
Test.startTest();
    LWCService.apiService = mockService;
    LWCService.MethodBeingTested();
Test.stopTest();
 

Where an instance dependency is being mocked, pass the mock object in via a constructor, or setter method.

//... class of method under test
public class AccountsService {
    private AccountsSelector selector;
    
    // Mock objects can be passed in during tests
    // instead of the production version of the selector.
    public AccountsService(AccountsSelector selector) {
        this.selector = selector;
    }
     
    public List<Account> methodBeingTested() {
        ...
    }
}
 
//...Passing in the mock object during a test.
Test.startTest();
    AccountsService service = new AccountsService(mockSelector);
    service.methodBeingTested();
Test.stopTest();

How method calls are matched to stubbed responses

When a mock object method is called during a test, ApexMocks tries to match the arguments supplied with a response:

  1. If no stubs have been set up for the method, it obviously won't find a match, so it will return null.
  2. Otherwise, it will work through every stub that has been set up for the method, starting from the last stub configured and working backwards to the first.
  3. As soon as a match is found, it will return the response associated with that match.
  4. If it doesn't find any matching stubs, it will again return null.

Stubs can either be set up so that the arguments must match exactly e.g. 'GBP' and 'USD' strings.

Or, "matchers" can be used that allow a looser, more generalised match.

// Setup
fflib_ApexMocks mocks = new fflib_ApexMocks();
MyMap mockMap = (MyMap)mocks.mock(MyMap.class);

mocks.startStubbing();
    // This will be matched last. It is the *most*
    // generalised matcher and can act as a way
    // of setting a default response.
    mocks.when(mockMap.get(fflib_Match.anyString()))
        .thenReturn('DEFAULT');
    mocks.when(mockMap.get(fflib_Match.stringEndsWith('.pdf')))
        .thenReturn('GENERALISED MATCH');
    // This will be tried first
    mocks.when(mockMap.get('CompanyAccounts.pdf'))
        .thenReturn('EXACT MATCH');
mocks.stopStubbing();

// Test
System.assertEquals('EXACT MATCH', mockMap.get('CompanyAccounts.pdf'));
System.assertEquals('GENERALISED MATCH', mockMap.get('AnnualReport.pdf'));
System.assertEquals('DEFAULT', mockMap.get('MeetingNotes.docx'));

Stubbing non-void methods

thenReturn()

thenReturn() sets up a single response for a set of matching arguments. The response can be of any return type, both literals and objects.

Calling a method stubbed with thenReturn() will return the same response no matter how many times it is called.

@isTest
private static void thenReturn_Example() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    IExchangeRateAPI mockApi =
        (IExchangeRateAPI)mocks.mock(ExchangeRateAPI.class);
    AccountsSelector mockSelector =
        (AccountsSelector)mocks.mock(AccountsSelector.class);

    mocks.startStubbing();

    // Can stub methods to return literal values
    mocks.when(mockApi.getLatestRate('GBP', 'USD'))
        .thenReturn(1.5);
    // ... or stub methods to return objects such as collections
    mocks.when(mockSelector
        .getAccountsWithIDs((Set<Id>)fflib_Match.anyObject()))
            .thenReturn(new List<Account>{
                new Account(Name = 'Netflix'),
                new Account(Name = 'Microsoft')
            });

    mocks.stopStubbing();

    // Test
    Decimal exchangeRate = mockApi.getLatestRate('GBP', 'USD');
    List<Account> selectedAccounts = mockSelector.getAccountsWithIDs(new Set<ID>{
        fflib_IDGenerator.generate(Account.SObjectType),
        fflib_IDGenerator.generate(Account.SObjectType)
    });

    // Assert
    System.assertEquals(1.5, exchangeRate);
    System.assertEquals(2, selectedAccounts.size());
    System.assertEquals('Netflix', selectedAccounts[0].Name);
    System.assertEquals('Microsoft', selectedAccounts[1].Name);
}

thenReturnMulti()

thenReturnMulti() sets up a sequence of varying responses for the same matching set of arguments.

In the following example, the ExchangeRateAPI's getLatestRate() method has been stubbed to return 1.0 on the first call, 1.5 on the second call and 2.0 on the third call.

The last configured response (in this case 2.0) will continue to be returned on all subsequent calls.

@isTest
private static void thenReturnMulti_Example() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    IExchangeRateAPI mockApi =
        (IExchangeRateAPI)mocks.mock(ExchangeRateAPI.class);

    mocks.startStubbing();

    // Can set up a series of different responses
    // for each successive call of the stubbed method.
    mocks.when(mockApi.getLatestRate('USD', 'GBP'))
        .thenReturnMulti(new List<Decimal>{ 1.0, 1.5, 2.0 });

    mocks.stopStubbing();

    // Test
    Decimal result1_0 = mockApi.getLatestRate('USD', 'GBP');
    Decimal result1_5 = mockApi.getLatestRate('USD', 'GBP');
    Decimal result2_0 = mockApi.getLatestRate('USD', 'GBP');

    Decimal resultExtraCall1 = mockApi.getLatestRate('USD', 'GBP');
    Decimal resultExtraCall2 = mockApi.getLatestRate('USD', 'GBP');

    // Assert
    System.assertEquals(1.0, result1_0);
    System.assertEquals(1.5, result1_5);
    System.assertEquals(2.0, result2_0);

    // Subsequent calls will continue to return the last stubbed response
    System.assertEquals(2.0, resultExtraCall1);
    System.assertEquals(2.0, resultExtraCall2);
}

thenThrow()

Mock object methods stubbed with thenThrow() will throw the configured exception when called with a given set of matching arguments.

The same exception will continue to be thrown on every subsequent matching call.

@isTest
private static void thenThrow_Example() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    IExchangeRateAPI mockApi =
        (IExchangeRateAPI)mocks.mock(ExchangeRateAPI.class);

    mocks.startStubbing();

    mocks.when(mockApi.getLatestRate('INVALID_PARAM', 'USD'))
        .thenThrow(new InvalidParamsException(
            'Params must be valid ISO currencies.'
        ));

    mocks.stopStubbing();

    // Test and Assert
    try {
        mockApi.getLatestRate('INVALID_PARAM', 'USD');
        System.assert(false, 'Should have thrown an exception.');
    } catch(InvalidParamsException ex) {
        System.assertEquals('Params must be valid iso currencies.', ex.getMessage());
    }
}
@isTest
private static void thenThrow_DMLException_Example() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    DBService service = (DBService)mocks.mock(DBService.class);

    mocks.startStubbing();
    mocks.when(service.saveAccount((Account)fflib_Match.anySObject()))
        .thenThrow(new DmlException());
    mocks.stopStubbing();

    try {
        service.saveAccount(new Account());
        System.assert(false, 'Should have thrown an exception here.');
    } catch(Exception ex) {
        System.assert(ex instanceof DmlException);
    }
}

thenThrowMulti()

thenThrowMulti() sets up a sequence of varying exceptions that will be thrown, in order, for the same matching call.

In the following example, the ExchangeRateAPI's getLatestRate() method has been stubbed to throw an RateLimit409Exception on the first matching call, then a ServerError500Exception on the second matching call.

The last configured exception, in this case the ServerError500Exception, will continue to be thrown for all subsequent matching calls.

@isTest
private static void thenThrowMulti_Example() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    IExchangeRateAPI mockApi =
        (IExchangeRateAPI)mocks.mock(ExchangeRateAPI.class);

    // Stubbing
    mocks.startStubbing();

    mocks.when(mockApi.getLatestRate('GBP', 'USD'))
        .thenThrowMulti(new List<Exception>{
            new RateLimit409Exception('Rate Limit'),
            ServerError500Exception('Bang')
        });

    mocks.stopStubbing();

    // Test and Assert
    try {
        mockApi.getLatestRate('GBP', 'USD');
        System.assert(false, 'Should have thrown an exception.');
    } catch(Exception ex) {
        System.assert(ex instanceof RateLimit409Exception);
        System.assertEquals('Rate Limit', ex.getMessage());
    }

    try {
        mockApi.getLatestRate('GBP', 'USD');
        System.assert(false, 'Should have thrown an exception.');
    } catch(Exception ex) {
        System.assert(ex instanceof ServerError500Exception);
        System.assertEquals('Bang', ex.getMessage());
    }

    try {
        mockApi.getLatestRate('INVALID_PARAM', 'USD');
        System.assert(false, 'Should have thrown an exception.');
    } catch(Exception ex) {
        // Subsequent calls will continue to throw the last configured Exception
        System.assert(ex instanceof ServerError500Exception);
        System.assertEquals('Bang', ex.getMessage());
    }
}

Stubbing void methods

doThrowWhen()

It's not possible to stub void methods with mocks.when(Object). This makes sense because you can't preprogram a response for a method that doesn't return anything.

However, you may still want to test scenarios where a void method throws an error.

The doThrowWhen() method accepts either a single Exception , or a List<Exception>, as argument.

Again, the final configured exception will continue to be thrown for an infinite number of subsequent calls.

@isTest
private static void doThrowWhen_MultiExceptionExample() {

    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    ILogger mockLogger = (ILogger)mocks.mock(Logger.class);

    LoggingException logExp1 = new LoggingException('Log exception 1');
    LoggingException logExp2 = new LoggingException('Log exception 2');
    
    // Stubbing
    mocks.startStubbing();

    List<LoggingException> loggingExceptions =
    	new List<LoggingException>{ logExp1, logExp2 };
    ((ILogger)mocks.doThrowWhen(loggingExceptions,  mockLogger))
        .log(fflib_Match.anyString());

    mocks.stopStubbing();

    // Test and Assert
    try {
        mockLogger.log('First log');
        System.assert(false, 'Should have thrown an exception.');
    } catch(LoggingException ex) {
        System.assertEquals('Log exception 1', ex.getMessage());
    }

    try {
        mockLogger.log('Second log');
        System.assert(false, 'Should have thrown an exception.');
    } catch(LoggingException ex) {
        System.assertEquals('Log exception 2', ex.getMessage());
    }

    try {
        mockLogger.log('Third log');
        System.assert(false, 'Should have thrown an exception.');
    } catch(LoggingException ex) {
        // Continues to return the last configured exception.
        System.assertEquals('Log exception 2', ex.getMessage());
    }
}

Chaining stubbing methods

Stubbing methods can be chained to set up more complex scenarios, where the response for the same matching call requires a different response type over time. For instance, the following example recreates a scenario where an API is down on the first call, but when it's retried it returns a success response.

//...stubbing
mocks.when(apiService.getExchangeRate('GBP', 'USD'))
    // On the first call the API is down.
    .thenThrow(new ServerError500Exception('Arggh!')
    // When it is retried, it is up again.
    .thenReturn(1.5);
    
// Test
Decimal rate;
try {
    // First call...
    rate = apiService.getExchangeRate('GBP', 'USD');
    System.assert(false, 'Should have thrown and exception here');
} catch(ServerError500Exception ex) {
    System.assert('Arggh!', ex.getMessage());
}

// Second call...
rate = apiService.getExchangeRate('GBP', 'USD');
System.assertEquals(1.5, rate);

GitHub repo with examples

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