Background
External Objects or the Salesforce Connect framework to be more precise are a way expose (view, search, modify ... essentially everything you can do with SObjects butnot quite) data that's stored outside a Salesforce org. Please refer to the followinglink for more information on what sort of data can be connected through Salesforce connect.
Personally the cost associated with a single external object connection and the uncertainty of maintaining a connection with the external data source make it a hard choice right of the bat. Alternatively having a middle ware to sync data periodically and having webservices exposed by the external system (or the middleware) to update the data from SFDC meets most client needs. However, in some cases Salesforce connect is the only choice, and in those cases it is important to know how to work with it in Apex in a maintainable way.
Problem
If you have never worked (written unit tests) with External Objects you will go under the assumption that Salesforce must have built some kind of abstraction to replicate dml behaviour when unit testing with External objects. Well you will be wrong and you might see an error similar to the one shown below. Once you come to that realization you will do some googling for a mocking framework for external objects. You will be disappointed but apparently there is something coming. But if we use the right design pattern we can facilitate mocking data pretty easily.
Encapsulation is key
From the research (very helpful article by Graham Barnard) I have done. The only way to work properly with external objects is to encapsulate it into a class (Model). The figure below illustrates a pattern that can be used. This is the only logical way of working with External Objects to facilitate unit test data creation with the additional benefit of localizing your queries to a single class (Model).
Example
The base class provides an elegant way of encapsulating any test related functionality and easily makes it accessible to all child models.
public with sharing virtual class ExternalObjectModelBase {
@TestVisible protected List<SObject> mockedRecords =
new List<SObject>();
public void addTestRecord(SObject record) {
mockedRecords.add(record);
}
public void addTestRecords(List<SObject> records) {
mockedRecords.addAll(records);
}
}
|
Sample implementation of the the base class using the Orders__x external object from the Salesforce Connect quickstart trailhead package. Making it Singleton is essential for inheriting from the base class.
public with sharing class ExternalOrderModel extends ExternalObjectModelBase{
private static ExternalOrderModel uniqueInstance;
//Private Constructor for Singleton
private ExternalOrderModel() {
}
//Singleton getter
public static ExternalOrderModel getInstance() {
if(uniqueInstance == null)
uniqueInstance = new ExternalOrderModel();
return uniqueInstance;
}
public Orders__x findByOrderId(Integer orderId) {
List<Orders__x> orderList = [SELECT customerID__c, orderDate__c,
orderID__c, shippedDate__c
From Orders__x
Where orderId__c =: orderId];
return (Test.isRunningTest()) ? (Orders__x) mockedRecords[0] :
(orderList.size() > 0) ? orderList[0] : null;
}
public List<Orders__x> findOrdersByCustomerId(Integer customerId) {
List<Orders__x> orderList = [SELECT customerID__c, orderDate__c,
orderID__c, shippedDate__c
From Orders__x
Where customerID__c =: customerId];
return (Test.isRunningTest()) ? (List<Orders__x>) mockedRecords :
(orderList.size() > 0) ? orderList : null;
}
}
|
Sample test class that covers the ExternalOrderModel. One thing to point out is, for the createTestData method we cannot use the @testSetup annotation since it runs in different context than the tests :(.
@isTest
private class ExternalOrderModelTest {
@isTest static void testFindByOrderId() {
createTestData();
Test.startTest();
Orders__x order = ExternalOrderModel.getInstance().findByOrderId(1);
System.assertEquals(order.ExternalId, '123');
System.assertEquals(order.OrderID__c, 1);
System.assertEquals(order.CustomerID__c, 123);
Test.stopTest();
}
@isTest static void testFindOrdersByCustomerId() {
createTestData();
Test.startTest();
List<Orders__x> orderList = ExternalOrderModel.getInstance().findOrdersByCustomerId(123);
System.assertEquals(orderList.size(), 2);
Test.stopTest();
}
private static void createTestData() {
//Create Order Data
Orders__x order1 = new Orders__x(
ExternalId = '123',
OrderID__c = 1,
CustomerID__c = 123,
orderDate__c = Date.today().addDays(-5),
shippedDate__c = Date.today().addDays(-3)
);
Orders__x order2 = new Orders__x(
ExternalId = '124',
OrderID__c = 2,
CustomerID__c = 123,
orderDate__c = Date.today().addDays(-5),
shippedDate__c = Date.today().addDays(-3)
);
List<Orders__x> orderList = new List<Orders__x>{order1, order2};
ExternalOrderModel extOrderModel = ExternalOrderModel.getInstance();
extOrderModel.addTestRecords(orderList);
}
}
|
Conclusion
Although this is not the only way of working with External object in Apex. I find it to be the best since it keeps the code localized and makes unit testing a breeze. Please let me know if you have any feedback or improvements. I hope this will be helpful for salesforce devs that are facing the same challenges as us with External Objects.
Comments