5

When unit testing code that interacts with SObjects in other managed packages, being able to mock those SObjects would be helpful so the packages don't have to be installed to run the unit tests. The context here is code that makes use of dynamic SOQL and the generic get/put methods of SObject i.e. code that is not tied to the other managed packages at compile time.

So it would be great if this was possible in tests:

@IsTest
private class MocksTest {

    @IsTest
    static void getSObjectType() {
        SObjectType t = Mocks.getSObjectType('abc__Def__c');
        System.assertNotEquals(null, t);
    }

    @IsTest
    static void newSObject() {
        SObject sob = Mocks.newSObject('abc__Def__c');
        System.assertNotEquals(null, sob);
    }
}

where the test could simulate some aspects of the missing SObjects:

public inherited sharing class Mocks {

    public class MyProvider implements System.StubProvider {
        public Object handleMethodCall(...) {
            ...
        }
    }

    public static SObjectType getSObjectType(String type) {
        return (SObjectType) Test.createStub(SObjectType.class, new MyProvider());
    }

    public static SObject newSObject(String type) {
        return (SObject) Test.createStub(SObject.class, new MyProvider());
    }
}

But per the Build a Mocking Framework with the Stub API documentation this doesn't work with these errors resulting instead:

MocksTest.getSObjectType
Fail
System.TypeException: Test.createStub() can only be called with classes in the current namespace

and:

MocksTest.newSObject
Fail
System.TypeException: Test.createStub() can only be invoked on user defined types.

This mattaddy / SObjectFabricator project handles part of this problem relies on real Type values so it can instantiate SObjects like this:

private Type sType;

public SObject toSObject() {
    return (SObject)JSON.deserialize(jsonString, sType);
}

I'm starting to think the only viable approach is to wrap SObjectType and SObject in local objects so I can mock those using Test.createStub or just implement the mocking directly. But that makes me think TDD (Test Driven Damage).

If you have tackled this problem, please share your solution.

(Ideally Test.createStub would be improved by Salesforce to support these two types, but presumably, that isn't trivial for them to do.)

  • Why does getting it out of the global describe not work, exactly? – Adrian Larson Feb 13 at 21:02
  • Good question - I've added the answer to the first sentence "the packages don't have to be installed to run the unit tests". – Keith C Feb 13 at 21:07
  • But using Schema.getGlobalDescribe() and then getting the SObjectType by name does in fact create a soft reference that will still compile if the other package is not installed. I don't really follow where the problem is. – Adrian Larson Feb 13 at 21:27
  • Compile - yes, but run (unit tests) - no. – Keith C Feb 13 at 21:36
  • 1
    +1 for mattaddy sobject fabricator which we use a lot. Even ApexMocks would have this issue as it is built atop StubApi. This is roughly akin to trying to mock selectors that return AggregateResult. That object can't be constructed so I ended up with a AggregateResultproxy object that I could mock (i.e. your "only viable approach..") – cropredy Feb 14 at 0:42
0

FYI, I took a look at putting wrappers around the SObject/SObjectType/SObjectField as below but on balance think this would be Test Driven Damage so won't go that route right now.

The root problem that got me looking at this is that patch orgs for managed packages that only make dynamic references to other packages don't get the other packages brought over and packages can't be installed. So there is a big testing and test coverage problem for patches. (The MOCKING mode would only need to be turned on in the patch orgs.)

/**
 * SObject wrapper objects to allow SObjects not present in the org to be mocked.
 * Test.createStub doesn't work for SObject/SObjectType/SObjectField.
 * Also Test.createStub doesn't support iinner classes so this code implements both real types and mock types.
 */
public inherited sharing class Sob {

    private class InvalidStateException extends Exception {
    }

    @TestVisible private static final Boolean MOCKING = false;

    // Start from here...
    public static Type getSObjectType(String type) {
        if (MOCKING) {
            return new Type(type);
        } else {
            SObjectType t = Schema.getGlobalDescribe().get(type);
            return t != null ? new Type(t) : null;
        }
    }

    // ... and here
    public static Field getSObjectField(Type type, String fieldName) {
        if (MOCKING) {
            return new Field(fieldName);
        } else {
            return new Field(type.t.getDescribe().fields.getMap().get(fieldName));
        }
    }

    // ... and here
    public static Sob.Obj[] query(Type t, Field f, Object[] values) {
        if (MOCKING) {
            // Add mock response data here
            return new Sob.Obj[] {};
        } else {
            String[] allFields = new List<String>(t.t.getDescribe().fields.getMap().keySet());
            SObject[] sobs = Database.query(''
                    + ' select ' + String.join(allFields, ', ')
                    + ' from ' + t.t
                    + ' where ' + f.f + ' in :values'
                    + ' order by Name'
            );
            Sob.Obj[] results = new Sob.Obj[] {};
            for (SObject sob : Security.stripInaccessible(AccessType.READABLE, sobs).getRecords()) {
                results.add(new Obj(t, sob));
            }
            return results;
        }
    }

    // Equivalent of SObjectType
    public class Type {

        private SObjectType t;
        private String type;

        private Type(SObjectType t) {
            this.t = t;
        }

        private Type(String type) {
            this.type = type;
        }

        public Obj newSObject() {
            if (t != null) return new Obj(this, t.newSObject());
            else if (type != null) return new Obj(this, new Map<String, Object>());
            else throw new InvalidStateException();
        }

        public Boolean equals(Object o) {
            Type that = (Type) o;
            if (t != null) return this.t == that.t;
            else if (type != null) return this.type == that.type;
            else throw new InvalidStateException();
        }

        public Integer hashCode() {
            if (t != null) return System.hashCode(t);
            else if (type != null) return System.hashCode(type);
            else throw new InvalidStateException();
        }
    }

    // Equivalent of SObjectField
    public class Field {

        SObjectField f;
        String field;

        private Field(SObjectField f) {
            this.f = f;
        }

        private Field(String field) {
            this.field = field;
        }

        public override String toString() {
            if (f != null) return String.valueOf(f);
            else if (field != null) return field;
            else throw new InvalidStateException();
        }

        public Boolean equals(Object o) {
            Field that = (Field) o;
            if (f != null) return this.f == that.f;
            else if (field != null) return this.field == that.field;
            else throw new InvalidStateException();
        }

        public Integer hashCode() {
            if (f != null) return System.hashCode(f);
            else if (field != null) return System.hashCode(field);
            else throw new InvalidStateException();
        }
    }

    // Equivalent of SObject
    public class Obj {

        Type type;

        SObject o;
        Map<String, Object> m = new Map<String, Object>();

        private Obj(Type type, SObject o) {
            this.type = type;
            this.o = o;
        }

        private Obj(Type type, Map<String, Object> m) {
            this.type = type;
            this.m = m;
        }

        public Type getSObjectType() {
            return type;
        }

        public Object get(String fieldName) {
            if (o != null) return o.get(fieldName);
            else if (m != null) return m.get(fieldName);
            else throw new InvalidStateException();
        }

        public Object get(Field field) {
            if (o != null) return o.get(field.f);
            else if (m != null) return m.get(field.field);
            else throw new InvalidStateException();
        }

        public Object put(String fieldName, Object value) {
            Object previous = get(fieldName);
            if (o != null) o.put(fieldName, value);
            else if (m != null) m.put(fieldName, value);
            else throw new InvalidStateException();
            return previous;
        }

        public Object put(Field field, Object value) {
            Object previous = get(field);
            if (o != null) o.put(field.f, value);
            else if (m != null) m.put(field.field, value);
            else throw new InvalidStateException();
            return previous;
        }
    }
}
| improve this answer | |

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.