Get 100% Code Coverage for Salesforce Custom Metadata Based Decisions

Get 100% Code Coverage for Salesforce Custom Metadata Based Decisions

January 18, 2018

How to obtain a full coverage for code which uses Custom Metadata for strategy-like decision implementation?

Introduction

Many applications use configuration data. Configuration data might be relevant to the entire organization, or a subset of user, or even different for each user. For the purposes of this article, we will focus only on global configuration settings.

Configuration data can be storedi in

  • Custom Objects,
  • Custom Settings and
  • Custom Metadata records.  

Custom objects are not normally used for the storage of configuration data, but in some extreme scenarios, such as the need for an encrypted field, custom objects may be used, since neither custom settings nor custom metadata support the encrypted field feature.

In most cases, custom settings or custom metadata are used.

Custom settings and custom metadata comparison.

Delivered in the Summer ‘15 release, Custom Metadata is a comparatively new feature and not well known amongst old-school Salesforce development veterans.  Although similar to List Custom Settings, it has several key differences.

Similarities.

  • Both List Custom Settings and Custom Metadata resemble Custom Objects. They contain table values and resemble a database table concept.
  • Both List Custom Settings and Custom Metadata allow creation of the following field types:
    • Checkbox,
    • Date,
    • Date\Time,
    • Email,
    • Number,
    • Percent,
    • Phone,
    • Text,
    • TextArea, and
    • URL.
  • Admin Users can create both List Custom Settings and Custom Metadata definitions using native UI Salesforce capabilities, as well as deploying List Custom Settings and Custom Metadata definitions using the Ant Migration Tool in the same way custom objects are deployed.
  • Users can create both List Custom Settings data and Custom Metadata records using native Salesforce UI capabilities.
  • Custom Settings and Custom Metadata provide the ability to set either Public or Protected Visibility. Protected Visibility prevents managed package users from accessing custom settings or custom metadata.

Differences.

table_Salesforce_custom_settings

Developers often prefer Custom Metadata records since they can be deployed.  During migration, some default settings might be migrated from one organization to another with metadata itself.

However, the ability to construct or insert custom metadata in Apex code can result in obstacles

to writing unit tests that cover business logic, and which depends on the custom metadata record values. Let’s consider the following example.

Example

Assume we need to build an application that calculates bank interest rates.  Some banks might use a simple interest rate calculation, while another might use a compound interest rate calculation.  We might be interested in building an application which would allow execution of either of these two calculations based on configuration.

Assume that we have created

  • a custom metadata “Custom_Metadata__mdt with a picklist field on it “Custom_Field__c”; and
  • a custom metadata record with “DeveloperName” value “Default which we are going to use to determine which calculation to use.

Also assume that we have created some simple class BankInterestCalculationLogic with the following code:

 

public class BankInterestCalculationLogic {
    public Double calculate(Double amount, Double interest, Integer numberOfYears) {
        Double result;
        List<Custom_Metadata__mdt> mdts = [ SELECT MasterLabel, Custom_Field__c FROM Custom_Metadata__mdt WHERE DeveloperName = 'Default' ];
        if ( mdts.size() == 1 && mdts[0].Custom_Field__c == '1') {
            // perform logic 1 - for example, simple calculation logic
            result = amount * ( 1 + interest * numberOfYears );
        } else {
            // perform logic 2
            result = amount * Math.pow( 1 + interest, numberOfYears );
        }
        return result;
    }
}

Salesforce_custom_settings

Although we might actually employ a strategy pattern to decide which formula to use for interest rate calculation, and which would still require some method to determine which strategy to use, we have opted to consider this piece of code for the sake of simplicity.

The main focus here is how to write a unit test that would use different strategies based on the current value in the custom metadata record.

The simplest attempt to write a test class for this might look like the following:

@isTest private class BankInterestCalculationLogicTest {
    private static testMethod void testCalculate() {
        Double revenue, expected, initial = 10000, rate = 0.12;
        Integer numberOfYears = 10;
        Test.startTest();
        	revenue = new BankInterestCalculationLogic().calculate(initial, rate, numberOfYears);
        Test.stopTest();
        Custom_Metadata__mdt mdt = [ SELECT MasterLabel, Custom_Field__c FROM Custom_Metadata__mdt WHERE DeveloperName = 'Default' ];
        if ( mdt.Custom_Field__c == '1') {
            // perform logic 1 - for example, simple calculation logic
            expected = initial * ( 1 + rate * numberOfYears );
        } else {
            // perform logic 2
            expected = initial * Math.pow( 1 + rate, numberOfYears );
        }
        System.assertEquals( expected, revenue, 'Calculated revenue should match to the expected value' );
    }
}

This test class would be executed successfully both in cases where the “Custom_Field__c field value in the record is 1 or 2 and would fail with error

System.QueryException: List has no rows for assignment to SObject

if someone were to delete this custom metadata record.

However, the main problem is that some piece of code would not be covered. If the value is 1, then code with a compound interest rate calculation would not be covered. Otherwise, if the value is 2, then the code with a simple interest rate would not be calculated.

Unfortunately official documentation does not provide a solution for how 100% of coverage can be achieved in this case.

Since we cannot insert a custom metadata record, despite the fact that users may play with them and delete and modify the data in the custom metadata record, and we cannot construct a custom metadata record to use inside “Test.loadData” method, we need another solution to overcome this obstacle with the usage of JSON.deserialize method.

Let’s generate a class to deal with Custom Metadata records:

public class CustomMetadataDAO {
    @testVisible static private Map<String, List<SObject>> customMetadataRecordsMap = new Map<String, List<SObject>>();
    
    public List<SObject> getCustomMetadataRecords(String query) {
        if ( !customMetadataRecordsMap.containsKey( query ) ) {
            customMetadataRecordsMap.put( query, Database.query( query ) );
        }
        return customMetadataRecordsMap.get( query );
    }
}

Now let’s generate the basic test for it.

@isTest public class CustomMetadataDAOTest {
    static testMethod void testGetMetadata() {
        List<SObject> 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<SObject> records ) {
        CustomMetadataDAO.customMetadataRecordsMap.put( query, records );
    }
}

The basic test gives 100% coverage for the CustomMetadataDAO class and also provides a utility method to set a custom metadata record for the test.  Look how can we use it to get 100% coverage to cover both interest rates calculations.

Let’s refactor our BankInterestCalculationLogic class like the following:

public class BankInterestCalculationLogic {
    public Double calculate(Double amount, Double interest, Integer numberOfYears) {
        Double result;
        List<Custom_Metadata__mdt> mdts = (List<Custom_Metadata__mdt>) new CustomMetadataDAO().getCustomMetadataRecords(
            'SELECT MasterLabel, Custom_Field__c FROM Custom_Metadata__mdt WHERE DeveloperName = \'Default\''
        );
        if ( mdts.size() == 1 && mdts[0].Custom_Field__c == '1') {
            // perform logic 1 - for example, simple calculation logic
            result = amount * ( 1 + interest * numberOfYears );
        } else {
            // perform logic 2
            result = amount * Math.pow( 1 + interest, numberOfYears );
        }
        return result;
    }
}

and test like the following.

@isTest private class BankInterestCalculationLogicTest {
    private static testMethod void testCalculateSimpleInterest() {
        Double revenue, expected, initial = 10000, rate = 0.12;
        Integer numberOfYears = 10;
        CustomMetadataDAOTest.setMetadata(
            'SELECT MasterLabel, Custom_Field__c FROM Custom_Metadata__mdt WHERE DeveloperName = \'Default\'',
            (List<Custom_Metadata__mdt>) JSON.deserialize( '[{"Custom_Field__c":"1"}]', List<Custom_Metadata__mdt>.class )
        );
        Test.startTest();
            revenue = new BankInterestCalculationLogic().calculate(initial, rate, numberOfYears);
        Test.stopTest();
        // perform logic 1 - for example, simple calculation logic
        expected = initial * ( 1 + rate * numberOfYears );
        System.assertEquals( expected, revenue, 'Calculated revenue should match to the expected value' );
    }
    
    private static testMethod void testCalculateCompoundInterest() {
        Double revenue, expected, initial = 10000, rate = 0.12;
        Integer numberOfYears = 10;
        CustomMetadataDAOTest.setMetadata(
            'SELECT MasterLabel, Custom_Field__c FROM Custom_Metadata__mdt WHERE DeveloperName = \'Default\'',
            (List<Custom_Metadata__mdt>) JSON.deserialize( '[{"Custom_Field__c":"2"}]', List<Custom_Metadata__mdt>.class )
        );
        Test.startTest();
            revenue = new BankInterestCalculationLogic().calculate(initial, rate, numberOfYears);
        Test.stopTest();
        Custom_Metadata__mdt mdt = [ SELECT MasterLabel, Custom_Field__c FROM Custom_Metadata__mdt WHERE DeveloperName = 'Default' ];
        // perform logic 2
        expected = initial * Math.pow( 1 + rate, numberOfYears );
        System.assertEquals( expected, revenue, 'Calculated revenue should match to the expected value' );
    }
}

Voila! We have 100% coverage and the test contexts are separated into two separate test methods!

Senior Engineer, CoreValue

Tags

Apex codeConfiguration dataCoreValueCustom MetadataCustom objectsCustom settingsDataDatabase table conceptDevelopmentList Custom SettingsSalesforceTable valuesTest method

Share


Recent Articles

Is Your Email Secure Enough?

February 16, 2018 | Anton Derevyanchenko

Methods to Minimize Spoofing and Forgery Throughout Your Workplace With the arrival of any transformative technology, those wishing to exploit it ethically or otherwise always arrive with it. The digital communication era has been no different. The nuisance of email spoofing and forgery threatens digital security on every level of society, from personal email to […]

CoreValue Services Welcomes Roman Dzvinka as a VP of Business Development

February 9, 2018

CoreValue Services, a software and technology company, is pleased to announce and welcome Roman Dzvinka as a Vice President of Business Development. Mr. Dzvinka, as a member of our management team, will be responsible for the leadership of company sales and marketing, developing new business opportunities and implementing company strategies for customer service. Igor Kruglyak, […]

© Copyright - CoreValue 2018
Salesforce, Sales Cloud, and others are trademarks of salesforce.com, inc., and are used here with permission.
Used with permission from Microsoft.