5

(Been spending quite a bit of time learning ApexMocks in conjunction with fflib pattern). Example simplified from more involved code

Class/method under test

public class Foo {
  public void doWork() {
     fflib_ISobjectUnitOfWork uow = Application.UnitOfWork.newInstance();
     uow.registerNew(new list<Account> {
           new Account(Name='A0', Website = 'www.salesforce.com'), 
           new Account(Name='A1', Website = 'www.google.com')
           };
     uow.registerNew(new list<Bar__c> {
           new Bar__c(Name = 'Bar0', FieldX__c = 'x0')
           }
     uow.commitWork();
  }
}

testMethod

private class TestApexMocks {
   @isTest private static void testUoW() {
     fflib_ApexMocks mocks = new fflib_ApexMocks();
     // Given mock implementation of UnitOfWork
     fflib_SobjectUnitOfWork mockUow    = (fflib_SobjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);
     Application.UnitOfWork.setMock(mockUow);

     // When method invoked
     new Foo().doWork();

     // Then verify new SObjects were created ... 

The only thing I could get to work was this convoluted approach using ArgumentCaptors...

fflib_ArgumentCaptor insertedSobjectsArgument= fflib_ArgumentCaptor.forClass(list<SObject>.class);
((fflib_SObjectUnitOfWork) mocks.verify(mockUow,2))
         .registerNew((SObject[])insertedSobjectsArgument.capture());                               

// extract the captured Sobjects arguments to the mocked UoW from a list<ANY>
Object[] actualInsertedSObjects = insertedSobjectsArgument.getAllValues();  

Account[] actualAccounts;
Bar__c[]  actualBars;
for (Object o: actualInsertedSObjects) {
        if (o instanceOf list<Account>)
            actualAccounts = new list<Account>((Account[])o);
        else
        if (o instanceOf list<Bar__c>)
            actualBars = new list<Bar__c>((Bar__c[])o);
    } 
// do asserts against actualAccounts and actualBars

I tried using the fflib_Match.sObjectWith matcher in a .verify but this matcher only applies to a single Sobject and registerNew is invoked with lists so there was no match.

The ArgumentCaptor method getAllValues() returns a list of Object where each Object is itself a list of SObject. I couldn't think of a cleaner way to cast this to the SObjects I cared about (Account, Bar__c)

There ought to be a way to do this with matchers and the .verify(..) method but I couldn't figure it out.

Editorial remark: The syntax for ApexMocks is not obvious for the beginner and there is a lot of casting required. Matchers are vital but the documentation examples don't tend to use collections and collections of SObjects so this can get frustrating

Answers/comments of the form - just query for the sobjects and assert are not on point. The point of the question is how to do this with ApexMocks, not how to test DML statements

3
  • I tried ApexMocks (stub api) out a bit, still wet behind the ears, but the blocker for me was I could not modify the returned mocked value during the test. I tried all sorts of various ways to return a different value based on a static variable and it never worked. I could only return values explicitly set it the mock class based on return type etc. That severely limited my ability to write good tests with the mock stub api. Looking forward to answer here. Thanks for the detail in this question
    – Eric
    Sep 15, 2017 at 2:45
  • @Eric I sense there is a gestalt in using ApexMocks where some things are straightforward once you learn to use the correct matchers so the thenReturn(..) value depends on the arguments passed to the mocked class method. I've been very successful with mocking selector methods to return sobject lists; I've also been successful at mocking service layer methods (void and non-void). But the holy grail was to mock both sobjects used in a service layer and updates to those sobjects so the test can execute without any setup DML nor actual CRUD DML yet all could be verified. Hence OP
    – cropredy
    Sep 15, 2017 at 5:17
  • 1
    @Eric - I just learned about fflib_Answer that might address your issue. See this blog
    – cropredy
    Sep 15, 2017 at 21:25

1 Answer 1

4

As of December 2017, a new matcher method has been added to ApexMocks: sObjectsWith(List<Map<Schema.SobjectField,Object>> expectedSobjects)

so, the testmethod now looks like this (no argument captors required):

fflib_ApexMocks mocks = new fflib_ApexMocks();
// Given mock implementation of UnitOfWork
fflib_SobjectUnitOfWork mockUow = (fflib_SobjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);
Application.UnitOfWork.setMock(mockUow);

// When method invoked
new Foo().doWork();

// Then verify new SObjects were created ... 
((fflib_SObjectUnitOfWork)mocks.verify(mockUow,mocks.times(1)
     .description('two accounts sb created')))
                .registerNew(fflib_Match.sObjectsWith(
                  new List<Map<SObjectField,Object>> {
                    new Map<SObjectField,Object> {
                       Account.Name => 'A0',
                       Account.Website => 'www.salesforce.com'
                    },
                    new Map<SObjectField,Object> {
                       Account.Name => 'A1',
                       Account.Website => 'www.google.com'
                    }
                 }
                ));

The list of maps is compared in the same order as the sobjects presented to registerNew(someList). That is, the verify will fail if you coded this in the testmethod:

((fflib_SObjectUnitOfWork)mocks.verify(mockUow,mocks.times(1)
     .description('two accounts sb created')))
                .registerNew(fflib_Match.sObjectsWith(
                  new List<Map<SObjectField,Object>> {
                    new Map<SObjectField,Object> {
                       Account.Name => 'A1',
                       Account.Website => 'www.google.com'
                    },
                    new Map<SObjectField,Object> {
                       Account.Name => 'A0',
                       Account.Website => 'www.salesforce.com'
                    }
                 }
                ));

because the sobjects were presented to registerNew in the code under test as A0 followed by A1.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .