[SalesForce] Unit Testing External Object with Mocked Data

New to the SF dev world so I am struggling a bit with setting up some unit tests on external objects. I have found very little info on this, most dating back a few years.

I did find a post here on SO where I have adapted some of the code to get a test working. This seems to do the trick, however, it is structured for only testing a single external object, where I have several I need to test in my unit test.

Here is my code structure so far. The idea behind it is that you can just have your class pass data through it normally, but you can also override it to pass the mocked data that I choose.

// QueryPassthrough.cls
public virtual inherited sharing class QueryPassthrough {
    public static List<SObject> records(List<SObject> records) {
        return instance.passThrough(records);
    }

    static QueryPassthrough instance = new QueryPassthrough();
    @TestVisible static void setMock(QueryPassthrough mock) {instance = mock;}

    protected virtual List<SObject> passThrough(List<SObject> records) {
        return records;
    }
}

// Test_EnterpriseUtilities.cls
@isTest
private class Test_EnterpriseUtilities {

    class Mock extends QueryPassthrough {
        final List<EntContact__x> externalContacts;

        Mock(List<EntContact__x> externalContacts) {
            this.externalContacts = externalContacts;
        }

        protected override List<SObject> passThrough(List<SObject> records) {
            return externalContacts;
        }
    }

    @isTest
    static testmethod void getExternalId() {
        Test.startTest();
            createData();
            String result = EnterpriseUtilities.GetExternalId('x0x3F00000HudxFQAR', 'EntContact__x');
        Test.stopTest();
        system.assertEquals('00T3F00000HudxFUAR', result, 'Expecting an ExternalId of 00T3F00000HudxFUAR from the mocked external contact');
    }

     static void createData() {

        // Create External Contact
        List<EntContact__x> mockExternalContacts = new List<EntContact__x>();
        mockExternalContacts.add(
            new EntContact__x(
                Name__c = 'Jim Test',
                ExternalId = '00T3F00000HudxFUAR'
            )
            );
        QueryPassthrough.setMock(new Mock(mockExternalContacts) );
     }

}

// EnterpriseUtilities.cls
@AuraEnabled(cacheable = true)
public static String GetExternalId(ID currentRecordId, String objName) {
    String query = 'SELECT ExternalId FROM ' + objName + ' WHERE id = :currentRecordId LIMIT 1';
    List<SObject> result = QueryPassthrough.records( Database.query(query) );
    String externalId = String.ValueOf( result[0].get('ExternalId') );

    return externalId != '' ? externalId : null;
}

While this works fine for the one external object I am mocking EntContact__x, I need to be able to support multiple objects. It was suggested to possibly use Map<SObjectType, List<SObject>> to handle multiples, but I am having trouble trying to figure out where to implement this.

My attempt, which failed, seems way off. Again, still trying to get the hang of this.

 final Map<SObjectType, List<SObject>> mockedData;
  Mock(Map<SObjectType, List<SObject>> mockedData) {
   this.mockedData = mockedData;
  }
  protected override Map<SObjectType, List<SObject>> passThrough(Map<SObjectType, List<SObject>> records) {
        return mockedData;
  }

// Fails with

@Override specified for non-overriding method: Map<Schema.SObjectType,List<SObject>> Test_EnterpriseUtilities.Mock.passThrough(Map<Schema.SObjectType,List<SObject>>)

Can anyone point out how I should implement the Map<SObjectType, List<SObject>>? End goal here is for my test class to be able to mock multiple external objects, where right now it is structured only to support the one.

Update:

Based on Answer

@isTest
private class Test_EnterpriseUtilities {
    class Mock extends QueryPassthrough {
        final Map<SObjectType, List<SObject> > cache;
        public Mock() {
            cache = new Map<SObjectType, List<SObject> >();
        }
        public Mock setDataStore(List<SObject> records) {
            cache.put(records.getSObjectType(), records);
            return this;
        }
        protected override List<SObject> passthrough(List<SObject> records) {
            return cache.get(records.getSObjectType() );
        }
    }
}

    /**
     * Create test data
     */
    static void createData() {
        // Test External Contact
        List<EntContact__x> mockExternalContacts = new List<EntContact__x>();
        mockExternalContacts.add(
            new EntContact__x(
            Name__c = 'Jim Test',
            ExternalId = '00T3G5430HudxFUAR'
            )
            );
        QueryPassthrough.setMock( new Mock(mockExternalContacts) );
    }

I may be missing something else here, but it is complaining about a constructor not being defined with these changes:

Constructor not defined: [Test_EnterpriseUtilities.Mock].<Constructor>(List<EntContact__x>) (115:35).

I left the QueryPassthrough.cls file unchanged and just made the update to the test.

Best Answer

Your cache is an entirely separate mechanism. The passthrough itself would just grab the appropriate collection.

protected override List<SObject> passthrough(List<SObject> records)
{
    return cache.get(records.getSObjectType());
}

Managing the cache itself isn't all that complicated.

@IsTest
public with sharing class QueryMock extends Query
{
    final Map<SObjectType, List<SObject>> cache;
    public QueryMock()
    {
        cache = new Map<SObjectType, List<SObject>>();
    }
    public QueryMock setDataStore(List<SObject> records)
    {
        cache.put(records.getSObjectType(), records);
        return this;
    }
}

You could add an overload signature which accepts the SObjectType too if you need it for better compatibility with your test data builder.

Notice that the constructor is empty (accepts no arguments). You need to set each data store via the fluent method provided.

QueryMock mock = new QueryMock()
    .setDataStore(new List<Case>())
    .setDataStore(new List<Lead>())
    .setDataStore(new List<My_External_Object__x());

Obviously, you would want to set up your data and populate those List with actual records.

Related Topic