5

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.');
}
6
  • 1
    now I'm a big user of apexmocks and I'm not sure I follow this. I'm not used to seeing static methods involved as apexmocks wants to mock objects and methods of objects. Have you looked at why don't my apexmocks work? or, for a non-fflib enterprise pattern example, this explanation of apexmocks and dependency injection . I've had no issues with overloaded methods defined in interfaces.
    – cropredy
    Dec 21 '19 at 6:04
  • Good to know its not necessarily the overloads then. I'll review your links and my code to see if I can get a better explanation/question in place.
    – Kamurai
    Dec 23 '19 at 13:59
  • The "short" version is that we have SecurityClass that has a method that checks if an object and object's field are valid for a given permission: we added a boolean parameter to that method, retaining the original signature (the original forwarding to the new one). We updated our mocking class methods, but the compiler doesn't seem to like how we're using the matchers now on the new method signature.
    – Kamurai
    Dec 23 '19 at 19:20
  • As mentioned elsewhere I believe my actual dilemma was that (somehow) I was previously able to mock with something like mockMethod(targetObject, fflib_match.anyString()), when the code specifically states that all arguments must be matchers if one is.<br><br> So my previously "working" code was actually broken, giving me a desired outcome that I can't seem to legally repeat, causing this question.<br><br> I believe the actual answer is "You can't mix matchers with normal variables in arguments."
    – Kamurai
    Dec 26 '19 at 20:18
  • yep - if mocking a method with 2+ args, if one arg is a matcher, all args must be matchers.
    – cropredy
    Dec 26 '19 at 20:29
1

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(..)
2
  • Thank you, I'll go back over this in detail. Part of answering a question is validating that it is the right question, which mine is looking more and more like it is not: I'm starting to think my previous code (that worked) was actually the broken code (as it got past the expected matchers code in fflib), which would invalidate my question entirely, I'll update when I confirm.
    – Kamurai
    Dec 26 '19 at 13:39
  • If nothing else, this answer is a proof for against the validity of my question and did help me towards an answer and viable code going forward.
    – Kamurai
    Dec 26 '19 at 20:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.