Monthly Archives: August 2019

Apex Mocks and Verifying Multiple Custom Type Arguments

Part five of a series. Posts include:

Let’s say you have an AccountsService with method doStuff that has two arguments: an fflib_SObjectUnitOfWork and a custom type Map. CancelRequest looks like this:

public class CancelRequest {
  Id id;
  Date cancelDate;
  String cancelReason;

  public CancelRequest(Id val) {this.id = val;}
  public CancelRequest withCancelDate(Date val) {this.cancelDate = val; return this;}
  public CancelRequest withCancelReason(String val) {this.cancelReason = val; return this;}
  public Date getCancelDate() {return this.cancelDate;}
  public String getCancelReason() {return this.cancelReason;}  
}

Now, let’s say you have some code that calls the AccountsService.cancel (this could be a Visualforce controller, invocable method, domain class method, batchable execute(), Apex REST class, etc. – for purposes of this example, it doesn’t matter).

public MyClass {
  public void doStuff(Set<Id> accountIds, String cancelReason) {
    fflib_ISobjectUnitOfWork uow = Application.UnitOfWork.newInstance();  
    Map<Id,CancelRequest> cancelRequests = new Map<Id,CancelRequest>();
    for (Id accountId: accountIds) {
      cancelRequests.put(accountId,new CancelRequest(accountId)
                                    .withCancelDate(Date.today())
                                    .withCancelReason(cancelReason) );
    }
    AccountsService.cancel(uow,cancelRequests);  // cancel accounts w/ date+reason
    uow.commitWork();
  }
}

To unit test MyClass.doStuff(..), you want to mock the AccountsService as all you’re really interested in is that it is called once and with the proper arguments. You have a separate unit test for the actual AccountsService.cancel that checks for the proper DML.

So, let’s build the testmethod…

@isTest
private static void givenAccountIdsAndReasonVerifyDelegationToAccountsServiceCancel() {
   fflib_ApexMocks mocks = new fflib_ApexMocks(); // framework
   
   // Given some accountIds
   Id[] mockAccountIds = new List<Id> {
          fflib_IdGenerator.generate(Account.SObjectType),
          fflib_IdGenerator.generate(Account.SObjectType)
  };

  // Given a cancel reason
  String cancelReason = 'foo';

  // Given a mockAccountsService (assumes standard naming conventions for service implementations
  AccountsServiceImpl mockAccountsService = 
      (AccountsServiceImpl) mocks.mock(AccountsServiceImpl.class);
  Application.Service.setMock(IAccountsService.class,mockAccountsService);

  // Given a mock Unit of Work
  fflib_SObjectUnitOfWork mockUow = 
    (fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);
  Application.UnitOfWork.setMock(mockUow);  

  // When doStuff called
  Test.startTest();
  new MyClass().doStuff(new Set<Id> (mockAccountIds),cancelReason);
  Test.stoptest();

  // Then verify service was called only once
  ((AccountsServiceImpl) mocks.verify(mockAccountsService,mocks.times(1)
                            .description('AccountsService.cancel sb called')))
           .cancel((fflib_ISobjectUnitOfWork) fflib_Match.anyObject(),
                   (Map<Id,CancelRequest>) fflib_Match.anyObject()
                  );
  //  Then verify that the service was called with the expected arguments.
  
  // Because the arguments are Apex custom types (and don't implement
  // an equals() and hashcode() method), there is no way for the ApexMocks
  // matchers to verify on equality. So, we fallback to argumentcaptors

  // Set up the captors, one per arg to method AssetsService.cancel
  fflib_ArgumentCaptor capturedUowArg = // arg0
    fflib_ArgumentCaptor.forClass(fflib_ISObjectUnitofWork.class);
  fflib_ArgumentCaptor capturedCancelRequestArg = // arg1
    fflib_ArgumentCaptor.forClass(Map<Id,CancelRequest>.class);

  // Capture the actual args used when the mock service was called
  ((AccountsServiceImpl) mocks.verify(mockAccountsService,1))
    .cancel((fflib_ISobjectUnitOfWork)capturedUowArg.capture(),
            (Map<Id,CancelRequest>)capturedCancelRequestArg.capture()
           );

  // Transform the capturedArgs (represented by type fflib_ArgumentCaptor)
  // into something we can inspect (using getValue() )
  fflib_ISobjectUnitOfWork actualUowArg =
    (fflib_ISobjectUnitOfWork) capturedUowArg.getValue();
 
  Map<Id,CancelRequest> actualCancelRequestArg =
    (Map<Id,CancelRequest>) capturedCancelRequestArg.getValue();
 
  // Now, whew, finally verify values
  System.assertEquals(mockAccountIds.size(),
          actualCancelRequestArg.size(),'all accts sb requested for cancel');

  for (Id accountId: actualCancelRequestArg.keySet() ) {
    System.assertEquals('Foo',
                        actualCancelRequestArg.get(accountId).getCancelReason());
    System.assertEquals(Date.today(),
                        actualCancelRequestArg.get(accountId).getCancelDate());
  }
}