Custom Metadata Types (CMDT) are a powerful tool for Salesforce developers, allowing you to manage application configurations that can be deployed across environments. However, when it comes to writing unit tests, they present a unique challenge: DML operations are not allowed on Custom Metadata.
If your Apex logic depends on specific Custom Metadata records, you might find yourself stuck. How do you test different configurations without actually creating records in your production org? In this guide, we will explore three effective ways to handle Salesforce Custom Metadata in unit tests, ranging from native instantiation to advanced mocking patterns.
The Challenge with Custom Metadata in Tests
Unlike standard SObjects, Custom Metadata is considered metadata, not data. This means:
1. Visibility: They are visible to unit tests by default, regardless of the SeeAllData annotation.
2. DML Restrictions: You cannot use insert, update, or delete on CMDT records in Apex.
3. Test Isolation: Because you can't insert them, you are often forced to rely on the records already existing in the environment, which breaks the principle of isolated, repeatable unit tests.
To overcome these hurdles, developers have found several ways to 'mock' these records into existence during a test execution.
Method 1: Native Instantiation (Winter '19 and Later)
Since the Winter '19 release, Salesforce has allowed developers to instantiate Custom Metadata records directly in memory. While this is a significant improvement, there is a catch: records instantiated this way cannot be retrieved via SOQL.
This method is perfect if your code accepts a record or a list of records as a parameter, rather than querying them internally.
public class CustomMetadataService {
/**
* This method instantiates a record in memory.
* Note: This record is NOT in the database and won't be found by SOQL.
*/
public MyCustomMetadataType__mdt getCustomMetadataRecord(String myName) {
MyCustomMetadataType__mdt theRecord = new MyCustomMetadataType__mdt();
theRecord.DeveloperName = myName;
theRecord.MasterLabel = myName;
return theRecord;
}
}
Pros: Native, clean, and supported by Salesforce. Cons: Does not help if your logic includes an inline SOQL query.
Method 2: The JSON Deserialization Hack
If your code performs a SOQL query and you need to mock the results, the most popular community workaround is the JSON Deserialization Hack. This technique allows you to set values for read-only fields (like those on Custom Metadata) by converting a JSON string back into an SObject.
To make this work, you need to structure your code to use a @TestVisible variable that holds the metadata records.
Step 1: Prepare your Class
public class DiscountService {
@TestVisible
private static List<Discount_Setting__mdt> settings {
get {
if (settings == null) {
settings = [SELECT DeveloperName, Discount_Percentage__c FROM Discount_Setting__mdt];
}
return settings;
}
set;
}
public Decimal applyDiscount(Decimal originalAmount) {
if (settings.isEmpty()) return originalAmount;
return originalAmount - (originalAmount * (settings[0].Discount_Percentage__c / 100));
}
}
Step 2: Mock in the Test Class
@IsTest
private class DiscountServiceTest {
@IsTest
static void testApplyDiscount() {
// Create a JSON representation of the metadata record
String mockMetadataJSON = '[{"attributes":{"type":"Discount_Setting__mdt"}, "DeveloperName":"Test_Setting", "Discount_Percentage__c":10.0}]';
// Deserialize and inject into the service class
DiscountService.settings = (List<Discount_Setting__mdt>) JSON.deserialize(mockMetadataJSON, List<Discount_Setting__mdt>.class);
Test.startTest();
Decimal result = new DiscountService().applyDiscount(100);
Test.stopTest();
System.assertEquals(90, result, "The 10% discount should be applied.");
}
}
Method 3: The Virtual Method / Wrapper Pattern
For enterprise-grade applications, the best practice is to abstract the metadata retrieval into a virtual method or a separate 'Selector' class. This follows the principle of Dependency Injection.
By making a retrieval method virtual and @TestVisible, you can override it in a test-specific subclass to return whatever mock data you need.
public virtual class MetadataProvider {
@TestVisible
virtual List<App_Config__mdt> getAllConfig() {
return [SELECT MasterLabel, Value__c FROM App_Config__mdt];
}
}
public class BusinessLogic {
MetadataProvider provider;
public BusinessLogic(MetadataProvider p) {
this.provider = p;
}
public void execute() {
List<App_Config__mdt> configs = provider.getAllConfig();
// Logic here...
}
}
In your test class, you simply create a 'Stub' of the MetadataProvider that returns your manually instantiated records from Method 1.
Common Mistakes to Avoid
- Relying on Production Data: Avoid writing tests that depend on specific records existing in the org. If someone deletes or renames a record in Production, your deployment pipeline will break.
- Forgetting System Fields: When using the JSON hack, remember that fields like
DeveloperNameandMasterLabelare often required for logic. Ensure they are included in your JSON string. - Namespace Issues: If you are developing a managed package, remember to include the namespace prefix in your JSON type attributes (e.g.,
pkg__MyMetadata__mdt).
Frequently Asked Questions
Can I use Test.loadData() for Custom Metadata?
No. Test.loadData() is designed for standard and custom SObjects. Since Custom Metadata does not support DML, you cannot load it via CSV in a test context.
Why does my SOQL query return no records in my test?
If you instantiated the record using new MyMetadata__mdt(), it exists only in memory. To make a SOQL query return mock records, you must use the variable injection strategy (Method 2) or a Selector pattern (Method 3).
Is it better to use Custom Settings or Custom Metadata for testing?
Custom Settings allow DML in test methods, making them easier to test. However, Custom Metadata is superior for deployment and ALM. Most developers prefer Custom Metadata and use the mocking techniques described above to bridge the testing gap.
Wrapping Up
Testing Custom Metadata requires a shift in mindset. Because we cannot rely on DML, we must rely on architectural patterns like JSON Deserialization or Dependency Injection. By abstracting your metadata access, you ensure that your unit tests remain fast, isolated, and independent of the specific data present in your Salesforce environment.
Whether you choose a quick JSON hack or a robust wrapper class, mocking your metadata will lead to more resilient code and a smoother CI/CD process.