0

I have a custom object called Document__c that looks up to Account. Account has a lookup field to another custom object called Business__c. In SOQL, the following query will return the id of Business__c:

List<Document__c> documents = [SELECT Id, Account__r.Business__c FROM Document__c];
Set<Id> businessIds = new Set<Id>();
For(Document d:documents) {
businessIds.add(d.Account__r.Business__c);

How can I write a test method without actually doing the query (to speed up test run times)? As far as I know, the only way to return relationships more than one level deep is to query for them. I know ApexMock has IDGenerator but cannot see how I can use it for this use case?

Please keep in mind that this is a simplified scenario for demonstration purposes.

  • 2
    Why would you want to? Create your Business__c object and associate it, no reason to "mock" this data.. This sounds like a xy problem – EricSSH Apr 18 '18 at 20:45
  • Clarified the question a bit more. Let me know if it still doesn't look right. – Jorjani Apr 18 '18 at 20:58
4

You can mock virtually any relationship if you use a Mock Data Layer Pattern.

Imagining your scenario, you could add an Interface to your main class. Let's call it IDataLayer. Add this new interface to the main class (here, called DataLayerHandler.cls)

public class DataLayerHandler {
    public interface IDataLayer {
        List<Document__c> getDocumentList();
    }
}    

Now, in the same class, let's create an inner class for this data layer, implementing the Interface you just created:

public class DataLayerHandler {
    // DataLayer Class: 
    public class DataLayer implements IDataLayer {
        List<Document__c> getDocumentList(){
            return [SELECT Id, Account__r.Business__c FROM Document__c];
        }
    }

    public interface IDataLayer {
        List<Document__c> getDocumentList();
    }
}

Ok, now how to access it? We need to create an instance of this DataLayer, and we will use the DataLayerHandler Class constructor for that. This is really important, I'll explain it below:

public class DataLayerHandler {

    // First, create the dataLayer variable:
    private IDataLayer dataLayer;

    // For an empty constructor, you create a new instance of the DataLayer: 
    public DataLayerHandler() {
        this(new DataLayer());
    }

    // If the constructor contains a dataLayer as a parameter, use it in your class. 
    public DataLayerHandler(IDataLayer dataLayer) {
        this.dataLayer = dataLayer;
    }

    // Here, you can access the data using the DataLayer: 
    public void proccessDocuments() {
        List<Document__c> documentsToProcess = dataLayer.getDocumentList();        
    }

    public class DataLayer implements IDataLayer {
        List<Document__c> getDocumentList(){
            return [SELECT Id, Account__r.Business__c FROM Document__c];
        }
    }

    public interface IDataLayer {
        List<Document__c> getDocumentList();
    }
}

But why to do all of that?! And this is the magic: because now you can pass a MOCK version of the data layer to you class to use!! Given this handler class, let's create a test class and a mock class, implementing the same interface:

@isTest
private class DataLayerHandler_Test {

    private static MockDataLayer dataLayer;

    public class MockDataLayer implements DataLayerHandler.IDataLayer {

        List<Account> documentAccounts = new List<Account>();
        List<Document__c> mockDocuments = new List<Document__c>();

        List<Document__c> getDocumentList(){
            return mockDocuments;
        }

    }
}

With the mock version of the data layer, you can start creating your mock records. Use the method getFakeId(Schema.SObjectType type) to create Ids to your mock records:

@isTest
private class DataLayerHandler_Test {

    private static MockDataLayer dataLayer;
    private static DataLayerHandler testedClass;

    static {
        dataLayer = new MockDataLayer();
    }

    @isTest
    private static void proccessDocumentsTest() {
        // consider using a Mock Data Factory, but for this example, let's create a test Account: 
        Account testAccount = new Account(
                Name = 'TestAccount', 
                Id = getFakeId(Schema.Account.SObjectType),
                Business__c = getFakeId(Schema.Business__c.SObjectType)
        );

        dataLayer.documentAccounts.add(testAccount);

        Document__c testDocument = new Document__c(
            Name = 'TestDocument',
            Id = getFakeId(Schema.Document__c.SObjectType),
            Account__r = testAccount                
        );

        dataLayer.mockDocuments.add(testDocument);

        // Now, all you need to do is to create a new instance of you handler class, but now, passing your mock data layer to the constructor!

        // Since the Interface structure is the same, the mock version is accepted by the class constructor.

        Test.startTest();
            testedClass = new DataLayerHandler(dataLayer);
            testedClass.proccessDocuments();
        Test.stopTest();

        //You can even use the Mock Repository to store the result of the method proccessDocuments(), to pass to your assertions. 

    }

    //Method to generate a fake Id based on the SBObject Type: 
    public static String getFakeId(Schema.SObjectType type) {
        String result = String.valueOf(fakeIdCounter++);
        return type.getDescribe().getKeyPrefix() + '0'.repeat(12 - result.length()) + result;
    }

    public class MockDataLayer implements DataLayerHandler.IDataLayer {

        List<Account> documentAccounts = new List<Account>();
        List<Document__c> mockDocuments = new List<Document__c>();

        List<Document__c> getDocumentList(){
            return mockDocuments;
        }

    }
}

There you go. This way you can mock several different types of relationships.

This is an extremely useful pattern and allow you to test your logic in different scenarios without running queries, without hitting the database. It's a good way to improve your tests performance.

Just don't forget to create some bulkified tests as well.

I hope that helps!

  • 1
    To be pedantic, the fourth and fifth character should also be 0. – sfdcfox Apr 19 '18 at 0:43
  • Hahaah in the fake Id, you mean? Moreover, what do you think about the suggestion? – Matheus Goncalves Apr 19 '18 at 4:09
  • 1
    I've used it before, along with a fake DB cache, in order to mock up data without DML operations, but it can take a while. It's certainly a good idea if you don't need real DML, though. – sfdcfox Apr 19 '18 at 4:15
  • The way you described in steps made the complex logic look simpler. Thanks. – Pranay Jaiswal Apr 20 '18 at 21:57
  • Glad to help, Pranay! – Matheus Goncalves Apr 21 '18 at 23:11

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.