Get 100% Code Coverage for Salesforce Custom Metadata Based Decisions
Many applications use configuration data. Configuration data might be relevant to the entire organization, or subset of users or might be different for each user. In other words, it is possible to define some global configuration relevant to the whole organization or some minor configuration which is relevant to some small departments. In this article we will focus only on global configuration settings.
There are different possible solutions for storing configuration data. Configuration data can be stored in
- Custom Objects,
- Custom Settings and
- Custom Metadata records.
Custom objects are not usually used for storage of configuration data but in some extreme edge-cases scenarios like if there is a need to have an encrypted text field, custom objects might be also used for configuration data since neither custom settings nor custom metadata support encrypted fields feature.
In most cases, however, custom settings or custom metadata are used.
Deprecated List custom settings and custom metadata comparison
Custom Metadata is a feature delivered back in Summer ‘15 release. It is similar to (Deprecated) List Custom Settings but it has several key differences. List Custom settings are deprecated since Spring ’18 release and custom metadata functionality should be used instead of them.
- Both List Custom Settings and Custom Metadata resemble Custom Objects. They contain table values and resemble database tables.
- Both List Custom Settings and Custom Metadata allow creation of Checkbox, Date, Date\Time, Email, Number, Percent, Phone, Text, Text Area, URL fields.
- Admin Users can create both List Custom Settings and Custom
- Metadata definition using Salesforce native UI capabilities as well as deploy List Custom Settings and Custom Metadata definition using Ant Migration Tool or SFDX the same way custom objects are deployed.
- Users can create both List Custom Settings data and Custom Metadata records using Salesforce native UI capabilities. However, since List Custom Settings are deprecated, this old functionality must be enabled on new environments if needed.
- You can select Visibility for both Custom Settings and Custom Metadata to have it either Public or Protected. Protected Visibility prevents managed package users from accessing the custom settings or custom metadata.
|List Custom Settings (deprecated)||Custom Metadata|
|List Custom Settings data can be constructed and inserted by Apex code.||Custom Metadata records cannot be neither constructed nor inserted by Apex code.|
|List Custom Settings data cannot be deployed||Custom Metadata records can be deployed|
|List Custom Settings support Currency fields.||Custom Metadata support Picklists, Metadata Relationships (similar to Custom Objects lookups) and Long Text Area Fields.|
|There are no specific Custom Settings fields values features related to managed package||If you develop a managed package you can control if Custom Metadata Field values can be changed by package user or by package developers|
Usually Custom Metadata records are very attractive to developers since they can be deployed.
So it is also better to use Custom Metadata configuration than Custom Settings configuration since Custom Metadata records can be deployed and during migration some default settings might be migrated from one organization to another with metadata itself. Since Spring ’18 List Custom Settings are deprecated, so Custom Metadata became the default option.
However, the fact that custom metadata records cannot be constructed or inserted in Apex code brings obstacles to the process of writing unit tests to cover business logic depending on the custom metadata record values. Let’s discuss this more deeply considering the following example.
Assume we need to build an application for bank interest rates calculation.
Some banks might use simple interest rate calculation, another might use compound interest rate calculation.
We might be interested in building an application which would allow us to use any of these two calculations based on configuration.
Assume we have created a custom metadata type
Custom_Metadata__mdt and created a picklist field on it
Custom_Field__c and created a custom metadata record with DeveloperName value Default which we are going to use to determine which calculation to use.
Assume we created some simple class
BankInterestCalculationLogic with the following code
We might actually employ a strategy pattern to decide which formula to use for interest rate calculation, but for sake of simplicity let’s consider this piece of code. Anyway, even using a strategy pattern we would have some method to determine which strategy to use.
The main focus here is how to write unit tests which would use different strategies based on current value in the custom metadata record.
Simplest try to write test class for this might look like following
This test class will succeed both in cases when the
Custom_Field__c field value in the record is 1 or 2 but will fail with error
System.QueryException: List has no rows for assignment to SObjectif someone deletes this custom metadata record.
However, the main problem is one of the branches of conditional statement will not be covered. If the value is 1 then the branch implementing the compound interest rate calculation will not be covered. Otherwise if the value is 2 then the branch implementing the simple interest rate would not be calculated.
Unfortunately official documentation doesn’t provide a solution how 100% of coverage can be achieved in this case.
We can’t insert custom metadata records (despite the fact that users may play with them and delete and modify the data in the custom metadata record).
We can’t even construct a custom metadata record to use it inside
However, there is a solution to this problem.
We might overcome this obstacle by using
Let’s generate a class to deal with Custom Metadata records
and basic test for it
Basic test gives 100% coverage for the
CustomMetadataDAO class and also provides a utility method to set custom metadata records for the tests.
Look how can we use it to get 100% coverage to cover both interest rates calculations.
Let’s refactor our BankInterestCalculationLogic class like following
BankInterestCalculationLogic class like following
and test like following
Voila! Now we have 100% coverage and test contexts for different logic branches are separated!