Verify non-deterministic field values in a unit test that does no DML

apexmockfflib

for fflib / apexmocks users

Let's say you have a class/method that inserts new objects where a field value is non-deterministic, say, a UUID

public class AccountsServiceImpl {
  public void makeAccount() {
    fflib_ISobjectUnitOfWork uow = Application.UnitOfWork.newInstance();
    uow.registerNew(new Account (Name = UUID.getV4());
    uow.commitWork();
  }
}

The method UUID.getV4() constructs a V4-compliant UUID using a Cryptographic random number

The normal testmethod ApexMocks pattern for verifying an sobject would be:

fflib_ApexMocks mocks = new fflib_ApexMocks();

fflib_SObjectUnitOfWork mockUow = (fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);  // the mock UnitOfWork
Application.UnitOfWork.setMock(mockUow); // injected for the factory to see

// When code-under-test executed
AccountsServiceImpl.makeAccount();

// Then verify Account fields constructed as expected
((fflib_SObjectUnitOfWork)mocks.verify(mockUow,mocks.times(1).description('account should be constructed as expected')))
        .registerNew(fflib_Match.sObjectWith(new Map<SObjectField,Object> {
            Account.Name => ??  // what goes here ??
    }));

But, you have no constant to assign to the Matcher sObjectWith for field Account.Name as the value is determined randomly.

Assuming you wish to avoid DML in this unit test method, what do you do?

Best Answer

There are two approaches to solve this

Dependency-injection of mock UUIDs into the UUID class

There would be all sorts of ways to do this including:

  • Modifying the UUID class to use an interface to supply UUIDs. The production code would use a concrete implementation that uses random numbers but the testmethod would inject an object that returned a predictable set of UUIDs, in a known ascending pattern. This is roughly analogous to the HttpMockCallout interface used to test callout responses

Your testmethod now looks like:

public class UUIDMocker {

  static Integer timesCalled = 0;
  public String uuidGenerator() {
     timesCalled++;
     return '00000000-0000-0000-0000-00000000000' + i; // assumes called 0-9 times; could be cleverer
  }
}
...
fflib_ApexMocks mocks = new fflib_ApexMocks();

fflib_SObjectUnitOfWork mockUow = (fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);  // the mock
Application.UnitOfWork.setMock(mockUow); // injected for the factory to see

// When code-under-test executed
AccountsServiceImpl.makeAccount();

// Then verify Account fields constructed as expected
((fflib_SObjectUnitOfWork)mocks.verify(mockUow,mocks.times(1).description('account should be constructed as expected')))
        .registerNew(fflib_Match.sObjectWith(new Map<SObjectField,Object> {
            Account.Name => '00000000-0000-0000-0000-000000000001'
    }));

Use ApexMocks ArgumentCaptor

ArgumentCaptors allow the object passed to uow.registerNew(theSObject) to be inspected during the testmethod execution. This works because since we are mocking the UnitOfWork object, the ApexMocks (i.e. Test.StubAPI) framework is capturing all of the arguments passed to every method in the mocked object (the uow). As the framework has captured the arguments, they can be inspected later.

The syntax is a bit wonky but it goes like this:

fflib_ApexMocks mocks = new fflib_ApexMocks();

fflib_SObjectUnitOfWork mockUow = (fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);  // the mock UnitOfWork
Application.UnitOfWork.setMock(mockUow); // injected for the factory to see

// When code-under-test executed
AccountsServiceImpl.makeAccount();

// Define an ArgumentCaptor
fflib_ArgumentCaptor capturedArg    = fflib_ArgumentCaptor.forClass(SObject.class); // N.B. type is ignored, see comments in fflib_ArgumentCaptor class
    

// Capture the argument in the registerNew call; we don't know how many registerNew calls so make sure to get all of them
((fflib_SObjectUnitOfWork) mocks.verify(mockUow,mocks.atLeastOnce()))
                               .registerNew((SObject)capturedArg.capture());

// Since the code under test could have called registerNew many times, get all values   
Object[] actualArgAsObjects = capturedArg.getAllValues();

// Since the code under test could have done registerNew for Sobjects besides Account, 
// filter out only those that are Accounts 
    for (Integer i = 0; i < actualArgAsObjects.size(); i++) { // for every registerNew()
        if (actualArgAsObjects[i] instanceof Account) {
            // Cast from Object to Account to make it easier to work with
            Account actualRegisterNewAccountArg = (Account) actualArgAsObjects[i];
            Pattern uuidV4Pattern = Pattern.compile('[\\w]{8}-[\\w]{4}-4[\\w]{3}-[89ab][\\w]{3}-[\\w]{12}');
            System.assertEquals(true,uuidV4Pattern.matcher(actualRegisterNewAccountArg.Name).matches(),'Account.Name is not a UUID');
        }
    }

Notes

1 - It is left to the reader to add assertions to verify that each constructed Account in the transaction has a different UUID generated (assuming the code under test creates 2+ Accounts) 2 - On the line fflib_ArgumentCaptor capturedArg = fflib_ArgumentCaptor.forClass(SObject.class); you would think you could do this:

fflib_ArgumentCaptor capturedArg    = fflib_ArgumentCaptor.forClass(Account.class); 

and capture only the registerNew of Account SObjects, ignoring any registerNew for Contact, Order, etc. But this sadly doesn't work as per this comment in the fflib_ArgumentCaptor class

  • The Type is IGNORED because we can't determine an object instance's Type at runtime unlike in Java.

  • Rigorous type checking may be introduced in a future release, so you should specify the expected argument type correctly.