apex-enterprise-patterns / fflib-apex-mocks

An Apex mocking framework for true unit testing in Salesforce, with Stub API support
BSD 3-Clause "New" or "Revised" License
423 stars 214 forks source link

Multiple domain mocks #139

Closed gesta22 closed 1 year ago

gesta22 commented 1 year ago

I was not able to create mocks for several domain classes in my test method.

Here is my unit test, that's not working:

    @isTest
    private static void testValidateParent() {

        Id mockCaseId = fflib_IDGenerator.generate(Case.SObjectType);
        Id mockEntityId = fflib_IDGenerator.generate(BPP_Entity__c.SObjectType);
        Set<Id> caseIds = new Set<Id>{mockCaseId};
        List<Case> caseList = new List<Case>{new Case(Id = mockCaseId, Subject = 'Hi', Status = 'Closed', Origin = 'Email')};
        List<BPP_Case_Entity_Association__c> junctionList = new List<BPP_Case_Entity_Association__c>{
            new BPP_Case_Entity_Association__c(BPP_Case__c = mockCaseId, BPP_Entity__c = mockEntityId)
        };
        String condition = 'test condition';

        fflib_ApexMocks mocks = new fflib_ApexMocks();
        BPP_CaseSelector mockCaseSelector = (BPP_CaseSelector)mocks.mock(BPP_CaseSelector.class);
        BPP_Cases mockCaseDomain = (BPP_Cases)mocks.mock(BPP_Cases.class);
        BPP_CaseEntityAssociations mockCaseEntityDomain = 
            (BPP_CaseEntityAssociations)mocks.mock(BPP_CaseEntityAssociations.class);

        mocks.startStubbing();
        mocks.when(mockCaseDomain.getBlockedCondition()).thenReturn(condition);
        mocks.when(mockCaseEntityDomain.getParentField()).thenReturn(BPP_Case_Entity_Association__c.BPP_Case__c);
        mocks.when(mockCaseSelector.sObjectType()).thenReturn(Case.SObjectType);
        mocks.when(mockCaseSelector.selectSObjectsById(caseIds)).thenReturn(caseList);
        mocks.when(mockCaseSelector.selectByIdWithCondition(caseIds, condition)).thenReturn(caseList);
        ((BPP_CaseEntityAssociations)mocks.doThrowWhen(
            new DmlException(BPP_ErrorMessages.CASE_ENTITY_ADD_DEL_NOT_ALLOWED), 
            mockCaseEntityDomain)).throwParentBlockedError(junctionList[0]);
        mocks.stopStubbing();

        BPP_Application.selector.setMock(mockCaseSelector);
        BPP_Application.domain.setMock((fflib_ISObjectDomain)mockCaseDomain);
        BPP_Application.domain.setMock((fflib_ISObjectDomain)mockCaseEntityDomain);

        try {
            Test.startTest();
            BPP_CaseEntityService.validateParent(junctionList);
            Test.stopTest();
        }
        catch (DmlException exc) {
            Assert.areEqual(
                BPP_ErrorMessages.CASE_ENTITY_ADD_DEL_NOT_ALLOWED,
                exc.getMessage(),
                'Throw parent blocked error message method was not called'
            );
        }
        ((BPP_Cases)mocks.verify(mockCaseDomain, 1)).getBlockedCondition();
        ((BPP_CaseSelector)mocks.verify(mockCaseSelector, 1)).selectByIdWithCondition(caseIds, condition);
    }

During the debug I found out that in following lines second domain.setMock() overwrites the firts one, so mocks for mockCaseDomain are not working, it calls actual code instead of mock.

        BPP_Application.selector.setMock(mockCaseSelector);
        BPP_Application.domain.setMock((fflib_ISObjectDomain)mockCaseDomain);
        BPP_Application.domain.setMock((fflib_ISObjectDomain)mockCaseEntityDomain);

I assume there should be some way to mock several domains, selectors etc. in one unit test, as it's quite common scenario that we need to get some info from several objects in one method. But I was not able to find any examples online, so not sure, if that's possibe at all. And if not, how would you overcome such scenarios?

Also, just a note, if I remove mockCaseDomain from this unit test and hard code actual condition I will receive from this domain, test works fine, so I assume, all coding is done correctly.

stohn777 commented 1 year ago

Hi @gesta22. Like you've done for the mockCaseSelector, you must mock that sObjectType() method for the Domains, enabling the factory to register the classes for the corresponding SObjectType.

Also as an observational note, the best practice for creating a mock is using the related Interface as opposed to the concrete class, although it works. The reasoning is ensuring that the interfaces match the classes. By mocking the concrete class it's possible the existence of an undocumented, in the interface, method could slip through testing. For instance, a developer might have added xyzMethod(...) to the class but forgot to add it to the interface, which would not be detected when testing with a mock of the concrete class.

An interpretation of the Issue's code using the interfaces.


fflib_ApexMocks mocks = new fflib_ApexMocks();
IBPP_CaseSelector mockCaseSelector = (IBPP_CaseSelector)mocks.mock(IBPP_CaseSelector.class);
IBPP_Cases mockCaseDomain = (IBPP_Cases)mocks.mock(IBPP_Cases.class);
IBPP_CaseEntityAssociations mockCaseEntityDomain = (IBPP_CaseEntityAssociations)mocks.mock(IBPP_CaseEntityAssociations.class);
gesta22 commented 1 year ago

If anyone interested, it got resolved after I stubbed this method for all domains: mocks.when(mockDomain.sObjectType()).thenReturn(MyObject__c.SObjectType);

gesta22 commented 1 year ago

@stohn777 Not sure I'm getting your point on interfaces. Class don't have to be related to just 1 interface. What if my domain class implements several interfaces?

stohn777 commented 1 year ago

@gesta22 Initially this thread was talking about Selectors, and the typical pattern is creating an interface for the selector that extends fflib_SObjectSelector. In the snippets of your code, you would have an IBPP_CaseSelector Interface. This is the what would be registered with the Application.Selector factory. It's true that the selector might implement other interfaces, but this would be the one that's relevant to the factory pattern.

Domains would generally exhibit the same pattern. The relevant interface would be the one extending fflib_ISObjectDomain and would be used to configure the pattern or leveraged when creating mocks.

I hope that provides a bit of clarification.