[SalesForce] Why are the matchers not working after adding an overloaded method

To help getting past our permission checks during unit tests to test actual functionality, I created a mocking class so people could just call the mocking method they needed from that class for the security class. This includes methods that would take only a "desired result boolean", and uses matchers for the object and field.

I'm now getting errors as if the system doesn't recognize the mocking.
How do I mock this properly between the overloaded signatures and matchers?

I'll detail as best I can, but please let me know if I can supply any additional information.

We added a parameter to the field level methods in the security class to skip redundant object checks, and (to retain the references to the original method signature) overloaded it so that there was the original signature

//Old Security Class method:
isReadable(Boolean desiredResult, SObjectType targetObjectType, String fieldString)

and that one just forwarded to the next with objectCheck = true,

//New Security Class method:
isReadable(Boolean desiredResult, SObjectType targetObjectType, String fieldString, Boolean objectCheck)

isReadable(true, Device.getSObjectType(), 'Courier', true)

This meant we had to update the mocking to handle both method signatures, and a result of true or false for the new one:

//Non matcher and 2 matcher variants of Mocking Class methods
@TestVisible
    private static SecurityInterface isReadable(Boolean mockValue, SObjectType targetType, String fieldName){
        return isReadable(mockValue, targetType, fieldName);
    }

@TestVisible
    private static SecurityInterface isReadable(Boolean mockValue, SObjectType targetType){
        return isReadable(mockValue, targetType, fflib_Match.anyString());
    }

@TestVisible
    private static SecurityInterface isReadable(Boolean mockValue){
        return isReadable(mockValue, fflib_Match.anySObjectType(), fflib_Match.anyString());
    }

//Old mock implementation
@TestVisible
    private static SecurityInterface mockSecurityClassIsReadable(Boolean mockValue, SObjectType targetType, String fieldName){
        setupSecurityService();

        MOCKS.startStubbing();
        MOCKS.when(MOCKED_SECURITY_CLASS.isReadable(targetType, fieldName)).thenReturn(mockValue);
        MOCKS.stopStubbing();

        return MOCKED_SECURITY_CLASS;
    }

//New mock implementation
@TestVisible
    private static SecurityInterface mockSecurityClassIsReadable(Boolean mockValue, SObjectType targetType, String fieldName){
        setupSecurityService();

        MOCKS.startStubbing();
        MOCKS.when(MOCKED_SECURITY_CLASS.isReadable(targetType, fieldName)).thenReturn(mockValue);
        MOCKS.when(MOCKED_SECURITY_CLASS.isReadable(targetType, fieldName, true)).thenReturn(mockValue);
        MOCKS.when(MOCKED_SECURITY_CLASS.isReadable(targetType, fieldName, false)).thenReturn(mockValue);
        MOCKS.stopStubbing();

        return MOCKED_SECURITY_CLASS;
    }

This also required that I update the CommonMocks file to include both versions of the Security Class Method:

public Boolean isReadable(SObjectType objectType, String field)
{
    return (Boolean) mocks.mockNonVoidMethod(this, 'isReadable', new List<Type> {System.Type.forName('SObjectType'), System.Type.forName('String')}, new List<Object> {objectType, field});

}

public Boolean isReadable(SObjectType objectType, String field, Boolean objectCheck)
{
        return (Boolean) mocks.mockNonVoidMethod(this, 'isReadable', new List<Type> {System.Type.forName('SObjectType'), System.Type.forName('String'), System.Type.forName('Boolean')}, new List<Object> {objectType, field, objectCheck});
}

But when I implement my tests, I'm getting errors:

1) Succeeds
2) "CMPL123.fflib_ApexMocks.ApexMocksException: The number of matchers defined (1). does not match the number expected (2)
If you are using matchers all arguments must be passed in as matchers.
For example myList.add(fflib_Match.anyInteger(), 'String') should be defined as myList.add(fflib_Match.anyInteger(), fflib_Match.eq('String'))."
3) "System.NullPointerException: Attempt to de-reference a null object"

The de-reference null object message is typical for when the mocked instance isn't found for the implementation.

@IsTest
private static void mockSecurityClassIsReadableDoesNotThrowsExceptionWhenTrue(){
    MockingClass.mockSecurityClassIsObjectReadable(true);
    MockingClass.mockSecurityClassIsReadable(true,TEST_OBJECT_TYPE,TEST_FIELD);
    Boolean result = false;

    Test.startTest();
        result = SecurityClass.IsReadable(TEST_OBJECT_TYPE, TEST_FIELD, TEST_OBJECT_MESSAGE, TEST_FIELD_MESSAGE);
    Test.stopTest();

    System.assertEquals(true,result,'SecurityClass did not return correct result.');
}

@IsTest
private static void mockSecurityClassIsReadableDoesNotThrowsExceptionWhenTrueUsingMatcherFormForObject(){
    MockingClass.mockSecurityClassIsObjectReadable(true);
    MockingClass.mockSecurityClassIsReadable(true,TEST_OBJECT_TYPE);
    Boolean result = false;

    Test.startTest();
        result = SecurityClass.IsReadable(TEST_OBJECT_TYPE, TEST_FIELD, TEST_OBJECT_MESSAGE, TEST_FIELD_MESSAGE);
    Test.stopTest();

    System.assertEquals(true,result,'SecurityClass did not return correct result.');
}

@IsTest
private static void mockSecurityClassIsReadableDoesNotThrowsExceptionWhenTrueUsingMatcherFormForObjectAndField(){
    MockingClass.mockSecurityClassIsObjectReadable(true);
    MockingClass.mockSecurityClassIsReadable(true);
    Boolean result = false;

    Test.startTest();
        result = SecurityClass.IsReadable(TEST_OBJECT_TYPE, TEST_FIELD, TEST_OBJECT_MESSAGE, TEST_FIELD_MESSAGE);
    Test.stopTest();

    System.assertEquals(true,result,'SecurityClass did not return correct result.');
}

Best Answer

Here's a service (BarService) with an overloaded method execute. In both methods, true is returned.

public class BarService {
    public Boolean execute(Date d) {return true;}
    public Boolean execute(Date d, Id uId) {return true;}
}

And here's another service (the code-under-test, named FooService with method-under-test doStuff()) that calls both overloaded BarService execute(..) methods based on a runtime parameter.

public class FooService {
    private final BarService barService;
    public enum Dispatch {method1Arg,method2Arg}
    private Dispatch method;
    public FooService(Dispatch method) {
        this.barService = new BarService();
        this.method = method;
    }
    public FooService (BarService mockBarService, Dispatch method) {
        this.barService = mockBarService;
        this.method = method;
    }

    public Boolean doStuff() {
        switch on this.method {
            when method1Arg {
                return this.barService.execute(Date.today());
            }
            when method2Arg {
                return this.barService.execute(Date.today(),UserInfo.getUserId());
            }
            when else {return null;}
        }
    }
}

And here is the testmethod for the code-under-test

static void testBehavior() {
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    // Given mock Service class
    BarService mockBarService = (BarService) mocks.mock(BarService.class);

    //  Given mock Service results for both 1 and 2 arg invocations
    mocks.startStubbing();
    mocks.when(mockBarService.execute(fflib_Match.anyDate()))
            .thenReturn(false);
    mocks.when(mockBarService.execute(fflib_Match.anyDate(),fflib_Match.anyId()))
            .thenReturn(false);
    mocks.stopStubbing();
    // Given a service that in turn calls BarService.execute w/ 1 arg
    //  use Dependency Injection so mock service is invoked
    FooService f1 = new FooService(mockBarService,FooService.Dispatch.method1Arg);

    // Given a service that in turn calls BarService.execute w/ 2 arg
    //  use Dependency Injection so mock service is invoked
    FooService f2 = new FooService(mockBarService,FooService.Dispatch.method2arg);

    //  when 1 arg use case
    System.assertEquals(false,f1.doStuff(),'sb result of mock for 1 arg BarService.execute(Date)');

    //  when 2 arg use case
    System.assertEquals(false,f2.doStuff(),'sb result of mock for 2 arg BarService.execute(Date,Id)');
}

Note that:

  • No mismatched matchers error
  • Mock service thenReturn works as expected as false is returned when underlying "real" method always returns true.
  • I did not use the fflib Enterprise Pattern factory Application class as it didn't look like you were using that pattern. Instead the code-under-test is supplied via dependency injection the mocked dependent service. Of course, you could use your own factory pattern.

Now, this is more of a proof that overloaded methods can be mocked rather than directly answering your specific code issue.

But the point is that

  • There is some code under test that depends on some dependent object method returning some value. In my example, this is FooService.doStuff()
  • Hence, you mock the dependent object's methods as they will be called by the code-under-test. Since FooService.doStuff() invokes BarService.execute() in both 1 and 2 arg use cases, both of these execute invocations must be mocked using mocks.when(..)
Related Topic