Apex Mocks avoiding insert when committing fflib Unit of Work

apexapexmockfflibunit-test

I'm new in Apex Mocks but I'm really excited about all it's advantages. I'm trying to build test without DML. Here is a method I'm trying to test at the moment:

    public Id createMmDocument(String senderId, String receiverId, Map<Id, Decimal> assetQtyByIds) {
        final AST_AccountSelector.AST_IAccountSelector accountSelector = AST_AccountSelector.newInstance();
        final Map<Id, Account> accountsMap = new Map<Id, Account>(accountSelector.selectById(new Set<Id>{senderId, receiverId}));
        Account sender = accountsMap.get(senderId);
        Account receiver = accountsMap.get(receiverId);
        
        validateMmCreatorDocumentData(sender, receiver);

        final fflib_ISObjectUnitOfWork uow = (fflib_SObjectUnitOfWork)AST_Application.unitOfWork.newInstance(
                new List<SObjectType> {
                        AST_Material_Document__c.SObjectType,
                        AST_Material_Document_Item__c.SObjectType,
                        AST_SerialNumber__c.SObjectType
                }
        );
        AST_Material_Document__c document = new AST_Material_Document__c(
                RecordTypeId = MM_MATERIAL_DOCUMENT_RECORD_TYPE_ID,
                AST_Document_Date__c = Date.today(),
                AST_Receiving_Contractor__c = receiverId,
                AST_InternalSender__c = senderId
        );
        uow.registerNew(document);

        final AST_AssetSelector.AST_IAssetSelector assetSelector = AST_AssetSelector.newInstance();
        final Map<Id, Asset> assets = new Map<Id, Asset>(assetSelector.selectWithQtyAvailableAndMaterialAssetIdById(assetQtyByIds.keySet()));
        List<AST_Material_Document_Item__c> documentItems = insertMmDocumentItems(document.Id, assets, assetQtyByIds, sender, receiver, uow);
        insertSerialNumbers(documentItems, assets, uow);
        uow.commitWork();
        return document.Id;
    }

I have couple of questions regarding the right approach when coding the unit test:

  1. There are three class private methods fired in this method. As they are private I'm not able to put them in class interface so I'm not able to mock them and verify – what is the right approach? In an example validateMmCreatorDocumentData(sender, receiver) checks permissions and throws when lack of permission and I would like to avoid this throwing when unit testing logic above. How this should be done?
  2. Two of this private methods fired contain Unit Of Work as parameter. Look at example of insertSerialNumbers(documentItems, assets, uow) Is this right approach and how to unit test this private methods while I can't mock uow as it is not received from Application class in this private methods?
  3. Test below fails with following exception:
Class.fflib_SObjectUnitOfWork.registerNew: line 271, column 1
Class.fflib_SObjectUnitOfWork.registerNew: line 245, column 1
Class.AST_MaterialDocumentServiceTest.createMmDocumentTest: line 698, column 1
10:27:09.4 (818440711)|FATAL_ERROR|System.NullPointerException: Attempt to de-reference a null object

and I'm not sure what is wrong as Match always returns null…?
4. And final question is: how to avoid result of uow.commitWork()? Firing test causes DML exception as it tries to insert records to database. I managed to avoid this behavior adding custom fflib_SObjectUnitOfWork.IDML like you can see below but I'm not sure it is right way?

@IsTest
    private static void createMmDocumentTest() {
        final Id senderId = fflib_IDGenerator.generate(Account.SObjectType);
        final Id receiverId = fflib_IDGenerator.generate(Account.SObjectType);
        final Id assetId = fflib_IDGenerator.generate(Account.SObjectType);
        final Map<Id, Decimal> assetQuantities = new Map<Id, Decimal>{
                assetId => 1.00
        };
        final Id shopAccountRecordTypeId = Schema.SObjectType.Account.recordTypeInfosByDeveloperName.get(FRM_Constants.MPK_RECORD_TYPE_DEV).getRecordTypeId();
        Account sender = new Account(Id = senderId, RecordTypeId = shopAccountRecordTypeId);
        sender = (Account) fflib_ApexMocksUtils.setReadOnlyFields(
                sender,
                Account.class,
                new Map<SObjectField, Object>{
                        Account.AST_MPK__c => 'Z0099'
                }
        );
        Account receiver = new Account(Id = receiverId, RecordTypeId = shopAccountRecordTypeId);
        receiver = (Account) fflib_ApexMocksUtils.setReadOnlyFields(
                receiver,
                Account.class,
                new Map<SObjectField, Object>{
                        Account.AST_MPK__c => 'Z0011'
                }
        );
        final fflib_ApexMocks mocks = new fflib_ApexMocks();
        final AST_AccountSelector accountSelectorMock = (AST_AccountSelector) mocks.mock(AST_AccountSelector.class);
        final fflib_ISObjectUnitOfWork uowMock = new fflib_SObjectUnitOfWork(new List<SObjectType>{
                AST_Material_Document__c.SObjectType,
                AST_Material_Document_Item__c.SObjectType,
                AST_SerialNumber__c.SObjectType
        }, new FakeDmlForMockingUnitOfWork());
        mocks.startStubbing();
        mocks.when(accountSelectorMock.sObjectType()).thenReturn(Account.SObjectType);
        mocks.when(accountSelectorMock.selectById(new Set<Id>{
                senderId, receiverId
        })).thenReturn(new List<Account>{
                sender, receiver
        });
        mocks.stopStubbing();
        AST_Application.selector.setMock(accountSelectorMock);
        AST_Application.unitOfWork.setMock(uowMock);

        AST_MaterialDocumentService.AST_IMaterialDocumentService service = AST_MaterialDocumentService.newInstance();
        Exception exceptionThrown;
        try {
            service.createMmDocument(senderId, receiverId, assetQuantities);
        } catch (Exception ex) {
            exceptionThrown = ex;
        }

        fflib_ApexMocks.ApexMocksException mockException;
        try {
            final Id mmMaterialDocumentRecordType = Schema.SObjectType.AST_Material_Document__c.recordTypeInfosByDeveloperName.get(AST_MaterialDocumentService.MM_RECORD_TYPE_DEV).getRecordTypeId();

//line 698 - below verification fails:
            ((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, mocks.times(1).description('New material document was not registered'))).registerNew(fflib_Match.sObjectWith(
                    new Map<SObjectField, Object> {
                            AST_Material_Document__c.RecordTypeId => mmMaterialDocumentRecordType,
                            AST_Material_Document__c.AST_Document_Date__c => Date.today(),
                            AST_Material_Document__c.AST_Receiving_Contractor__c => receiverId,
                            AST_Material_Document__c.AST_InternalSender__c => senderId
                    }
            ));

            ((fflib_ISObjectUnitOfWork) mocks.verify(uowMock, mocks.times(1).description('Unit of work not committed'))).commitWork();
        } catch (fflib_ApexMocks.ApexMocksException ex) {
            mockException = ex;
        }
        System.assertEquals(null, mockException, MOCK_VERIFY_FAILED);
    }

    public class FakeDmlForMockingUnitOfWork implements fflib_SObjectUnitOfWork.IDML {
        public void dmlDelete(List<SObject> objList) {
        }
        public void dmlInsert(List<SObject> objList) {
        }
        public void dmlUpdate(List<SObject> objList) {
        }
        public void emptyRecycleBin(List<SObject> objList) {
        }
        public void eventPublish(List<SObject> objList) {
        }
    }
}

Best Answer

There are three class private methods fired in this method. As they are private I'm not able to put them in class interface so I'm not able to mock them and verify - what is the right approach?

  • I would mock the public methods these private methods call (selectors, etc)
  • I would make the private methods @TestVisible and then test them directly with the public methods they rely on returning mocked results

Two of this private methods fired contain Unit Of Work as parameter. Look at example of insertSerialNumbers(documentItems, assets, uow) Is this right approach and how to unit test this private methods while I can't mock uow as it is not received from Application class in this private methods?

  • Basically same answer as above, mock the unit of work and call the private methods directly, passing in the mocked UoW

Test below fails with following exception ...

There can be lots of reasons for this including an improper mocking setup. Consult My ApexMocks Aren't Working, What Can be Wrong

And final question is: how to avoid result of uow.commitWork()?

You need to mock the UnitOfWork. The standard pattern for this is:

// Given mock Uow
fflib_SObjectUnitOfWork mockUow = (fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);
// Given mock UoW injected
Application.UnitOfWork.setMock(mockUow);

You are overthinking things by including an object that implements IDML. You should only need a custom object implementing IDML for Production code where the standard fflib DML order (inserts first, then updates, then deletes) needs to be different

Related Topic