3

OK here is a real fun one.

I need to write tests that cover code that queries for custom metadata types. The code and test will be deployed to many different salesforce organizations which will contain entirely different records of this custom metadata.

The non-test code is querying the real custom metadata, but I need to test with all possible versions of the custom metadata, not just the records that actually exist. I need to do this for code coverage, error handling, and also so that I can deploy the same code across multiple organizations.

You can Instantiate Custom Metadata Types in Apex, but this does not help, because my code is querying the real custom metadata records, and it does not recognize records that I instantiate in memory in the test class.

I have seen the technique of using properties to store custom metadata values, but I can't see a way to apply that to this scenario.

public class DOMapUtil {

    public static Map<String, String> DOtoNoReply = new map <String, String>();
    public static Map<String, String> DOtoAdmEmail = new map <String, String>();
    public static Map<String, String> DOtoProgName = new map <String, String>();
    public static Map<String, String> DOtoSchoolName = new map <String, String>();
    public static Map<String, String> DOtoUnivName = new map <String, String>();
    public static Map<String, String> DOtoOARS = new map <String, String>();


    public static List<Map<String, String>> CreateDOMaps(){

        List<Map<String, String>> DOMaps = new List<Map<String, String>>();

        for(Degree_Offering__mdt DegOff  : [SELECT MasterLabel, 
                                            No_Reply_Email__c,
                                            Admissions_Email__c,
                                            Program_Name__c,
                                            School_Name__c,
                                            University_Name__c,                                                   
                                            OARS_Endpoint__c                                               
                                            FROM Degree_Offering__mdt]) {

             DOtoNoReply.put(DegOff.MasterLabel, DegOff.No_Reply_Email__c);
                DOMaps.add(DOtoNoReply);
                DOMaps.add(0,DOtoNoReply); 
             DOtoAdmEmail.put(DegOff.MasterLabel, DegOff.Admissions_Email__c);                
                DOMaps.add(1,DOtoAdmEmail);                                   
             DOtoProgName.put(DegOff.MasterLabel, DegOff.Program_Name__c);                
                DOMaps.add(2,DOtoProgName);                                
             DOtoSchoolName.put(DegOff.MasterLabel, DegOff.School_Name__c);                
                DOMaps.add(3,DOtoSchoolName);                         
             DOtoUnivName.put(DegOff.MasterLabel, DegOff.University_Name__c);                
                DOMaps.add(4,DOtoUnivName);                                
             DOtoOARS.put(DegOff.MasterLabel, DegOff.OARS_Endpoint__c);                 
                DOMaps.add(5,DOtoOARS);                                                  
        }            
        return DOMaps;       
    }  

Test class:

@IsTest
public class PayPalTest {

    @TestSetup
    public static void CreateOpps(){

        List<Opportunity> Opps = new List<Opportunity>();      
        String DegOff = [SELECT MasterLabel FROM Degree_Offering__mdt].get(0).MasterLabel;       
        /*Degree_Offering__mdt DegOff = new Degree_Offering__mdt(MasterLabel='jsf-xyz',
                                                               Admissions_Email__c = 'SupaChill@admissions.com',
                                                               No_Reply_Email__c = 'noreply@2u.com',                       
                                                               Program_Name__c = 'JSF@Harvard',
                                                               School_Name__c = 'JSF School of Awesome',
                                                               University_Name__c = 'JSF University',
                                                               OARS_Endpoint__c = 'http://requestbin.net/r/tu1n40tu')*/        

        for (Integer i=0; i<1; i++){            
            Opportunity Opp = new Opportunity(Name = 'Opp', Degree_Offering__c = DegOff, StageName = 'Prospecting', CloseDate = date.today(),
                                              Email_Address__c = 'test@2u.com'
                                             );
            Opps.add(Opp);           
        }            
        insert Opps;

    }

    @IsTest       
    public static void UpdateOpps(){   

        List<Opportunity> UpdatedOpps = [SELECT ID FROM Opportunity];

        for (Opportunity Opp : UpdatedOpps){
            Opp.Application_Fee_Waiver_Decision__c = 'Approved';            
            Opp.Application_Fee_Status__c = 'Paid Electronically';              
            Opp.Intent_To_Enroll__c = 'Enrolled';
            Opp.Enrollment_Deposit_Status__c = 'Paid Electronically';                           
        }

            update UpdatedOpps; //Needs to run outside start/stop test to prevent uncommitted work error 

        Test.startTest();           
            OARSCalloutMock Mock = new OARSCalloutMock();           
            Test.setMock(HttpCalloutMock.class, Mock); ////Needs to run in start/stop test to prevent uncommitted work error        
        Test.stopTest();

        Integer invocations = Limits.getEmailInvocations();     
            system.debug('Email invocations = '+ invocations);
        system.assertEquals(3, invocations);

            system.debug('Mock.callouts = '+ Mock.callouts);
        system.assertEquals(2, Mock.callouts); //Won't work unless Test.setMock callout is run synchronously               
    }

String DegOff only queries existing meta-data records, and instantiating Degree_Offering__mdt DegOff does not create something that can be queried by the public class DOMapUtil.

Not an easy one to explain, hit me up with any questions. Thanks for any thoughts!

  • 1
    corevalue.net/code-coverage-for-custom-metadata-based-decisions Is this what you're looking for? Where you can kind of control what the query for custom metadata actually returns even if it doesn't exist (ex. CustomMetadataDAO) – Kris Goncalves Oct 2 '19 at 0:29
  • Thanks, I did run across this, but the code there does not even compile, and it's difficult to understand how to fix it or then apply it. For example, what is "(List)" doing in this context? List mdts = (List) new CustomMetadataDAO().getCustomMetadataRecords – number41 Oct 2 '19 at 5:53
  • I'll add it as an answer although Aidan also provided a good one – Kris Goncalves Oct 2 '19 at 11:28
3

I can see the benefits of the CustomMetadataDAO approach from the comments above, but I tend to go a bit more direct. Since custom metadata records stay the same for a whole transaction, I tend to make them static, TestVisible, and lazy-instantiated.

e.g.

@TestVisible
private static List<Degree_Offering__mdt> degreeOfferingMetadata {get {
    if(degreeOfferingMetadata == null) {
        degreeOfferingMetadata = [
                SELECT MasterLabel, 
                No_Reply_Email__c,
                Admissions_Email__c,
                Program_Name__c,
                School_Name__c,
                University_Name__c,
                OARS_Endpoint__c
                FROM Degree_Offering__mdt
        ];
    }
    return degreeOfferingMetadata;
} set;}

public static List<Map<String, String>> CreateDOMaps(){

    List<Map<String, String>> DOMaps = new List<Map<String, String>>();

    for(Degree_Offering__mdt DegOff  : degreeOfferingMetadata) {
// ...

Then, I can just set them to whatever my test needs them to be e.g.

@IsTest       
public static void UpdateOpps(){
    DOMapUtil.degreeOfferingMetadata = new List<Degree_Offering__mdt>{
        new Degree_Offering__mdt(...)
    };
//...
| improve this answer | |
  • This is great, thanks – number41 Oct 2 '19 at 18:07
3

https://corevalue.net/code-coverage-for-custom-metadata-based-decisions/

You could essentially create a class to handle the querying of all your custom metadata and a test class for it that would allow you to create the records you want the query to return if need be. This class would check a map variable you set in test execution to see if there's a matching key-value for the query you're using. This lets you set a certain query to return a specific custom metadata record you create in the test while also being able to query real custom metadata if you want. Essentially, you're mocking the results of the soql query in test execution without affecting your real code

public class CustomMetadataDAO {
    @testVisible static private Map> customMetadataRecordsMap = new Map>();

    public List getCustomMetadataRecords(String query) {
        if ( !customMetadataRecordsMap.containsKey( query ) ) {
            customMetadataRecordsMap.put( query, Database.query( query ) );
        }
        return customMetadataRecordsMap.get( query );
    }
}


@isTest 
public class CustomMetadataDAOTest {
    static testMethod void testGetMetadata() {
        List customMetadataRecords;
        Test.startTest();
        customMetadataRecords = new CustomMetadataDAO().getCustomMetadataRecords( ‘SELECT 
        MasterLabel FROM Custom_Metadata__mdt’ );
        Test.stopTest();
        System.assertEquals( [ SELECT MasterLabel FROM Custom_Metadata__mdt ].size(), 
        customMetadataRecords.size(), ‘Size should match’ );
}

    public static void setMetadata( String query, List records ) {
        CustomMetadataDAO.customMetadataRecordsMap.put( query, records );
    }
}

In your actual, real code: use the class above to perform your soql queries for Custom Metadata. Since you're not setting any values in customMetadataRecordsMaps, it'll just query the database like normal

//since customMetadataRecordsMap in CustomMetadataDAO has no values, this method will just query the database with the string you pass in the method like normal
List<Degree_Offering__mdt> degreeOffering = new CustomMetadataDAO().getCustomMetadataRecords('SELECT MasterLabel, 
                                            No_Reply_Email__c,
                                            Admissions_Email__c,
                                            Program_Name__c,
                                            School_Name__c,
                                            University_Name__c,                                                   
                                            OARS_Endpoint__c                                               
                                            FROM Degree_Offering__mdt');

In your test, use the setMetadata method to put the query that'll be called in the real class and your test-created custom metadata records into the map so you are getting the records you want for a given query when it runs.

@IsTest
public static void UpdateOpps(){
    //Setting the map customMetadataRecordsMap in CustomMetadataDAO. When this query is 
    called in your code, the class will return whatever custom metadata record you create 
    in the 2nd parameter as the results.
    CustomMetadataDAOTest.setMetadata('SELECT MasterLabel, 
                                            No_Reply_Email__c,
                                            Admissions_Email__c,
                                            Program_Name__c,
                                            School_Name__c,
                                            University_Name__c,                                                   
                                            OARS_Endpoint__c                                               
                                            FROM Degree_Offering__mdt', new Degree_Offerring__mdt(MasterLabel = 'whatever_you_want'...))

}
| 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.