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

fflib_MethodArgValues equals method not working correctly. #67

Closed gdoenlen closed 4 years ago

gdoenlen commented 6 years ago

I suspect this mainly has to do with SFDC's implementation of equals for the List class as that is what the class relies on for it's equals (argValues == argValues). I believe is boils down to the list equals method not working correctly when comparing objects instantiated with SObjectType.newSObject and their regular constructors.

I have a class that instantiates a List<SObject> and populates its contents via SObjectType.newSObject(Id) which then gets passed to a Repository class which I use to update the records, and this class is what I mock and verify against that the right values were passed to it to update. If I do not verify a list with contents instantiated via newSObject equals will fail even though the values are equivalent.

I've provided my test below to show how I'm verifying. Using the commented functionality before the verify call will cause an ApexMocksException to be thrown because ff_lib_MethodArgValues.equals(Object) returns false even though equivalent values will be passed to it. I've verified this via debug logs:

12:25:18.1 (143519714)|USER_DEBUG|[84]|DEBUG|in count calls
12:25:18.1 (143608102)|USER_DEBUG|[85]|DEBUG|fflib_MethodArgValues:[argValues=((Account:{Id=001000000000001AAA, Number_of_Child_Accounts__c=200}))]
12:25:18.1 (143660625)|USER_DEBUG|[86]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accounts__c=200}))
12:25:18.1 (143827303)|USER_DEBUG|[90]|DEBUG|in arg values
12:25:18.1 (143880279)|USER_DEBUG|[91]|DEBUG|fflib_MethodArgValues:[argValues=((Account:{Id=001000000000001AAA, Number_of_Child_Accounts__c=200}))]
12:25:18.1 (143926744)|USER_DEBUG|[92]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accounts__c=200}))
12:25:18.1 (144008510)|USER_DEBUG|[48]|DEBUG|in other
12:25:18.1 (144040162)|USER_DEBUG|[56]|DEBUG|comparing arg values
12:25:18.1 (144074134)|USER_DEBUG|[57]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accounts__c=200}))
12:25:18.1 (144102713)|USER_DEBUG|[58]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accounts__c=200}))
12:25:18.1 (144118216)|USER_DEBUG|[59]|DEBUG|using equals
12:25:18.1 (144314303)|USER_DEBUG|[60]|DEBUG|false
12:25:18.1 (146248321)|EXCEPTION_THROWN|[81]|fflib_ApexMocks.ApexMocksException: Expected : 1, Actual: 0 -- Wanted but not invoked: Repository__sfdc_ApexStub.upd(List<SObject>).
@IsTest
    static void childAccountCountShouldBeRolledIntoParentAccount() {
        fflib_ApexMocks mocks = new fflib_ApexMocks();
        Repository repo = (Repository) mocks.mock(Repository.class);
        RollupSelector sel = (RollupSelector) mocks.mock(RollupSelector.class);
        Id parentId = fflib_IDGenerator.generate(Account.SObjectType);

        Map<Id, Account> trgNewMap = new Map<Id, Account>();
        for (Integer i = 0; i < 200; i++) {
            Id accountId = fflib_IDGenerator.generate(Account.SObjectType);
            trgNewMap.put(accountId, new Account(Id = accountId, ParentId = parentId));
        }

        mocks.startStubbing();
        mocks.when(sel.findByLookupIdIn(new Set<Id>{ parentId }))
             .thenReturn(trgNewMap.values());
        mocks.stopStubbing();

        Test.startTest();
        AccountAfterExec exec = new AccountAfterExec(repo, sel, trgNewMap, null);
        exec.execute();
        Test.stopTest();

        /*
         * We have to instantiate the verify list the same way
         * we do in the Rollup class as there seems to be a bug
         * in the equals method when instantiating with `newSobject`
         * and doing it regularly.
         */
        List<SObject> ret = new List<SObject>();
        SObject acc = Account.SObjectType.newSObject(parentId);
        acc.put(Account.Number_of_Child_Accounts__c.getDescribe().getName(), 200);

        ret.add(acc);

        /* THIS DOES NOT WORK
            List<SObject> ret = new List<SObject>{ new Account(
            Id = parentId,
            Number_of_Child_Accounts__c = 200
        )}; */
        ((Repository) mocks.verify(repo)).upd(ret);
    }

Please let me know if I have no been clear enough or if you require more details.

dfruddffdc commented 6 years ago

This is a known issue and you're right that's its down to the sobject implementation for equals(..). An sobject is only equal to another if every field value is equal.

The solution is to use matches for stubbing and verification. Try fflib_match.sobjectWithId(..) or fflib_match.refEq(..).

On Tue, 16 Oct 2018, 17:49 George Doenlen, notifications@github.com wrote:

Non-FF Sender

I suspect this mainly has to do with SFDC's implementation of equals for the List class as that is what the class relies on for it's equals (argValues == argValues). I believe is boils down to the list equals method not working correctly when comparing objects instantiated with SObjectType.newSObject and their regular constructors.

I have a class that instantiates a List and populates its contents via SObjectType.newSObject(Id) which then gets passed to a Repository class which I use to update the records, and this class is what I mock and verify against that the right values were passed to it to update. If I do not verify a list with contents instantiated via newSObject equals will fail even though the values are equivalent.

I've provided my test below to show how I'm verifying. Using the commented functionality before the verify call will cause an ApexMocksException to be thrown because ff_lib_MethodArgValues.equals(Object) returns false even though equivalent values will be passed to it. I've verified this via debug logs:

12:25:18.1 (143519714)|USER_DEBUG|[84]|DEBUG|in count calls 12:25:18.1 (143608102)|USER_DEBUG|[85]|DEBUG|fflib_MethodArgValues:[argValues=((Account:{Id=001000000000001AAA, Number_of_Child_Accountsc=200}))] 12:25:18.1 (143660625)|USER_DEBUG|[86]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accountsc=200})) 12:25:18.1 (143827303)|USER_DEBUG|[90]|DEBUG|in arg values 12:25:18.1 (143880279)|USER_DEBUG|[91]|DEBUG|fflib_MethodArgValues:[argValues=((Account:{Id=001000000000001AAA, Number_of_Child_Accountsc=200}))] 12:25:18.1 (143926744)|USER_DEBUG|[92]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accountsc=200})) 12:25:18.1 (144008510)|USER_DEBUG|[48]|DEBUG|in other 12:25:18.1 (144040162)|USER_DEBUG|[56]|DEBUG|comparing arg values 12:25:18.1 (144074134)|USER_DEBUG|[57]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accountsc=200})) 12:25:18.1 (144102713)|USER_DEBUG|[58]|DEBUG|((Account:{Id=001000000000001AAA, Number_of_Child_Accountsc=200})) 12:25:18.1 (144118216)|USER_DEBUG|[59]|DEBUG|using equals 12:25:18.1 (144314303)|USER_DEBUG|[60]|DEBUG|false 12:25:18.1 (146248321)|EXCEPTION_THROWN|[81]|fflib_ApexMocks.ApexMocksException: Expected : 1, Actual: 0 -- Wanted but not invoked: Repository__sfdc_ApexStub.upd(List).

@IsTest static void childAccountCountShouldBeRolledIntoParentAccount() { fflib_ApexMocks mocks = new fflib_ApexMocks(); Repository repo = (Repository) mocks.mock(Repository.class); RollupSelector sel = (RollupSelector) mocks.mock(RollupSelector.class); Id parentId = fflib_IDGenerator.generate(Account.SObjectType);

    Map<Id, Account> trgNewMap = new Map<Id, Account>();
    for (Integer i = 0; i < 200; i++) {
        Id accountId = fflib_IDGenerator.generate(Account.SObjectType);
        trgNewMap.put(accountId, new Account(Id = accountId, ParentId = parentId));
    }

    mocks.startStubbing();
    mocks.when(sel.findByLookupIdIn(new Set<Id>{ parentId }))
         .thenReturn(trgNewMap.values());
    mocks.stopStubbing();

    Test.startTest();
    AccountAfterExec exec = new AccountAfterExec(repo, sel, trgNewMap, null);
    exec.execute();
    Test.stopTest();

    /*         * We have to instantiate the verify list the same way         * we do in the Rollup class as there seems to be a bug         * in the equals method when instantiating with `newSobject`         * and doing it regularly.         */
    List<SObject> ret = new List<SObject>();
    SObject acc = Account.SObjectType.newSObject(parentId);
    acc.put(Account.Number_of_Child_Accounts__c.getDescribe().getName(), 200);

    ret.add(acc);

    /* THIS DOES NOT WORK            List<SObject> ret = new List<SObject>{ new Account(            Id = parentId,            Number_of_Child_Accounts__c = 200        )}; */
    ((Repository) mocks.verify(repo)).upd(ret);
}

Please let me know if I have no been clear enough or if you require more details.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/financialforcedev/fflib-apex-mocks/issues/67, or mute the thread https://github.com/notifications/unsubscribe-auth/ANciZ_JI4WJerRWsNxo3vfkqOiQHNalFks5ulg4JgaJpZM4XexQG .

afawcett commented 4 years ago

Thanks @gdoenlen for the detailed summary! Also if you want to continue to discuss. 👍