Mocking Salesforce Apex Methods with ApexMocks Verify

This post provides an overview of the ApexMocks library’s verify() method, with accompanying code examples.

We will be mocking the FXService (foreign exchange service) Apex class that gets currency exchange rate data from an api.

public class FXService {

    public Decimal getRate(String fromCurr, toCurr) {
        // Simplified api code...
        return api.getRate(fromCurr, toCurr);
   }
}

Introduction

During the execution of a Salesforce Apex test method, ApexMocks records all interactions with mock objects. It registers which methods are called, how many times they are called, and with which arguments.

Decimal result1 = mockFX.getRate('USD', 'GBP');
Decimal result2 = mockFX.getRate('USD', 'GBP');
Decimal result3 = mockFX.getRate('USD', 'EUR');
// getRate count with 'USD', 'GBP: 2
// getRate count with 'USD', 'EUR': 1
// getRate count with 'USD' as first arg: 3

The verify() method allows a developer to set up expectations about the underlying behaviour of the unit of code they are testing. When executed, verify() will query the recorded method invocation data and pass or fail the test depending on the set constraints.

// Assertions and expectations at end of a unit test

// Verify there were 2 calls with 'USD', 'GBP' arguments
((FXService)mocks.verify(mockFX, 2))
	.getRate('USD', 'GBP');

// Verify there was 1 call with 'USD', 'EUR' arguments
((FXService)mocks.verify(mockFX, 1))
	.getRate('USD', 'GBP');

// Verify there were 3 calls with 'USD' as the first argument,
// and any other currency as the 2nd argument
((FXService)mocks.verify(mockFX, 3))
	.getRate(
        fflib_Match.eqString('USD'), 
        fflib_Match.anyString());

// Verify the getTimeSeriesData() was never called.
// Note: fflib_ApexMocks.NEVER == 0
((FXService)
    mocks.verify(mockFX, fflib_ApexMocks.NEVER))
    .getTimeSeriesData(fflib_Match.anyString());

Test Setup When Using verify()

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

2. Create a mock object for the dependency.

FXService mockFX = (FXService)mocks.mock(FXService.class);

3. During test execution, the unit under test will interact with the mock object (instead of calling the real API).

// Unit under test...
Integer maxRetries = 3;
Integer retryCount = 0;
while(retryCount < maxRetries) {
    try {
        // During test execution, fxService will
        // be the mock object created in step 2.
        Decimal rate = fxService.getRate(fromCurr, toCurr);
        response.isSuccess = true;
        response.data = rate;
    } catch(Exception ex) {
        retryCount++;
        response.isSuccess = false;
        response.message = Label.FXError;
    }
}

4. Use verify() to confirm that the unit under test behaved as expected.

// Assert state where this is accessible.
System.assertEquals(false, response.isSuccess);
System.assertEquals(Label.FXError, response.message);

// Verify underlying behaviour that isn't
// accessible in the state.
// e.g. Verify that the retry mechanism tried the
// api 3 times (before it eventually failed).
((FXService)mocks.verify(mockFX, 3))
    .getRate('USD', 'GBP');

The verify() method

The verify() method has three overloaded versions.

The first version specifies the exact number of times that a method and matching arguments must be invoked for the test to pass.

Object verify(Object mockInstance, Integer times)

A second version is a shortcut to say that just a single matching invocation is expected.

Object verify(Object mockInstance)

In other words, it is the equivalent of:

verify(mockInstance, 1)

Verification Modes

The third version of verify() allows a developer to pass in a verification mode.

Object verify(Object mockInstance, fflib_VerificationMode verificationMode)

The verification mode determines how strictly verify() will interpret the count of matching invocations.

Under the hood, the first two versions of verify() use the times(int) verification mode, as shown in the following extract from the fflib_ApexMocks class.

// From the fflib_ApexMocks class
public Object verify(Object mockInstance, Integer times) {
    return verify(mockInstance, this.times(times));
}
fflib_VerificationMode times(Integer)

times(int) requires actual counts to exactly match the supplied argument.

// Passing counts: 3 only
((FXService)mocks.verify(mockFX, mocks.times(3))
    .getRate('USD', 'GBP');

A number of other verification modes are available (accessed via the fflib_ApexMocks instance), that configure looser counting strategies.

fflib_VerificationMode atLeast(Integer)

atLeast(int) requires actual counts to at least equal the supplied argument, but a higher count is also acceptable.

// Passing counts: 2, 3, 4, 5, ... to infinity
((FXService)mocks.verify(mockFX, mocks.atLeast(2))
    .getRate('USD', 'GBP');
fflib_VerificationMode atLeastOnce()

atLeastOnce() is a shortcut for atLeast(1).

// Passing counts: 1, 2, 3, 4, ... to infinity
((FXService)mocks.verify(mockFX, mocks.atLeastOnce())
    .getRate('USD', 'GBP');
fflib_VerificationMode atMost(Integer)

atMost(int) requires the actual count to be less than or equal to the supplied argument. However, if there are no matching calls at all the expectation will fail. This could better be thought of as “at least one, but at most x”.

// Passing counts: 1, 2, or 3
// Note: 0 will fail
((FXService)mocks.verify(mockFX, mocks.atMost(3))
    .getRate('USD', 'GBP');
fflib_VerificationMode between(Integer, Integer)

between(int, int) requires the actual count to be between the supplied min and max arguments (inclusive).

// Passing counts: 1, 2, or 3
((FXService)mocks.verify(mockFX, mocks.between(1, 3))
    .getRate('USD', 'GBP');
fflib_VerificationMode never()

never() can be used to verify that no matching calls occurred.

// Passing counts: 0
((FXService)mocks.verify(mockFX, mocks.never())
    .getRate('USD', 'GBP');
fflib_VerificationMode description(String)

description(String) can be chained with one of the other verification modes to specify a custom error message if the expectation fails.

// Providing an on-fail message
((FXService)mocks.verify(
    mockFX,
    mocks.never()
        .description('getRate() should never have been called with "USD" and "GBP"'))
    .getRate('USD', 'GBP');