36

I have a controller that is querying custom metadata. I'm attempting to write unit tests to test the queries. Is it possible to insert test custom metadata for the purposes of unit tests?

1

7 Answers 7

38

At time of writing, it is not possible to insert test custom metadata, unless you use the Metadata API which would also present a challenge in a test scenario. That said, you should not need to. Custom Metadata is Metadata and as such is not affected by the SeeAllData annotation. See this article dedicated to testing custom metadata.

2
  • 10
    I would argue the "you should not need to" part solely for the purpose that, in a unit test, I should be able to insert a record for custom metadata (similar to custom setting) and then verify that my code uses the same value. I've voted for the idea mentioned above by Daniel Ballinger and it's now "In Development" so waiting for it :)
    – markgarg
    Dec 20, 2018 at 11:24
  • 2
    I don't disagree :) Dec 20, 2018 at 15:07
22

New in Winter 19! Instantiate Custom Metadata Types in Apex

Example from the release notes...

How: In this example, the first method is instantiating a custom metadata record, but no records are inserted into memory. The second method retrieves a record, changes it, and returns it to the caller, but the change is not updated in the database.

public class CustomMetadataService {

    public CustomMetadataService() {}

    /**
     * This method instantiates a custom metadata record of type MyCustomMetadataType__mdt
     * and sets the DeveloperName to the input String.
     * Note that the record is not inserted into the database, 
     * and would not be found by a SOQL query.
     */
    public MyCustomMetadataType__mdt getCustomMetadataRecord(String myName) {
        MyCustomMetadataType__mdt theRecord = new MyCustomMetadataType__mdt();
        theRecord.DeveloperName = myName;
        return theRecord;
    }

    /**
     * This method retrieves a custom metadata record, changes a field, and returns it
     * to the caller. Note that the changed record is not updated in the database.
     */
    public MyCustomMetadataType__mdt getChangedCustomMetadataRecord(String myNewName) {
        MyCustomMetadataType__mdt theRecord = [SELECT Id, DeveloperName from MyCustomMetadataType__mdt LIMIT 1];
        theRecord.DeveloperName = myNewName;
        return theRecord;
    }

}
1
  • 3
    Have you tried this while creating unit test for a class which uses custom metadata?
    – ad08
    Sep 19, 2019 at 9:32
11

There is possible hack to work around impossibility of insertion of custom metadata records.

For this hack you just need to put query for custom metadata into separate property which should be settable for unit tests.

@testVisible static List<Custom_Metadata__mdt> customMetadata { 
    get {
        if ( customMetadata == null )
            customMetadata = [ SELECT All_Needed_Custom_Fields__c FROM Custom_Metadata__mdt ]; 
        return customMetadata;
    } set; }

Then you can use JSON.deserialize method to setup these metadata records. This method allows you to overcome 'field is not writable' error.

Example of code you could use is the following:

CustomClass.customMetadata  = (List<Custom_Metadata__mdt>) JSON.deserialize( '{"All_Needed_Custom_Fields__c ":"dummyValue"}', List<Custom_Metadata__mdt>.class )

Also you may find useful to read my article which provides more generic and advanced solution to this problem.

4
  • Very nice. Though it didn't work for me when I put this code in a test setup method, I had to include it in the test methods themselves. Jan 8, 2019 at 8:27
  • 1
    @SanderdeJong, yes, you can't use it in testSetup since this is not real insert operation, however I designed this to be included in the service class itself (the property declaration) and in the test method you just redefine value of this property in each test method, since you might need different values. If you need one, you can set it in auxiliary method and call it from each test method
    – Patlatus
    Jan 9, 2019 at 9:06
  • Also: which columns you select can make or break code. MasterLabel is crucial, omitting it will break things. And the system is very picky on which other columns you can use. I am still figuring out how to make it work properly and fully for my situation. Jan 9, 2019 at 10:58
  • In my case it was enough to use just customField I needed and my code was working fine without querying system fields; you might need MasterLabel, DeveloperName system fields depending on your needs
    – Patlatus
    Jan 9, 2019 at 13:39
10

As stated above, you can't insert Custom Metadata via Apex, but you create instances of mock data via JSON deserialization.

public static SObject getMetadata(SObjectType metadataSObjectType, Map<String, Object> fields) {
    /**
     * @Version-1.0.0
     *      Created.
     */
    Map<String, Object> jsonFields = new Map<String, Object> {
        'attributes' => new Map<String, Object> {
            'type' => metadataSObjectType.getDescribe().getName()
        }
    };
    if(fields != null) {
        jsonFields.putAll(fields);
    }
    return (SObject) JSON.deserialize(JSON.serialize(jsonFields), SObject.class);
}

Update

If you need to query Custom Metadata in an Apex Class, consider using a private, virtual, @TestVisible method which returns queried Metadata. If I'm use Custom Metadata, often it's because I'm building some sort of framework out of an abstract class.

@TestVisible virtual Framework_Request__mdt[] getAllMetadata() {
    return [
        SELECT Id, MasterLabel, Request__c
        FROM Framework_Request__mdt
        WHERE Framework__c = :this.getType().getName()
        ORDER BY Order__c ASC
    ];
}

public abstract Type getType();

// This Framework dynamically creates Requests from Metadata
public Result processRequests() {
    Result result = new Result();
    try {
        for(Framework_Request__mdt metadata : this.getAllMetadata()) {
            Type requestType = Type.forName(metadata.Request__c);
            Object newInstance = requestType == null ? null : requestType.newInstance();
            if(newInstance instanceof Request) {
                Request request = (Request) newInstance;
                request.setData(this.getData());
                request.process();
            }
        }
        result.setIsSuccess(true);
    }
    catch(Exception e) {
        result.setException(e);
        result.setIsSuccess(false);
    }
    return result;
}

Then when testing the Framework, you need need to create an implementation to test your abstract class. There you can pass in hard-coded Custom Metadata so you don't have to reply on Custom Metadata existing

@IsTest    
public class Framework_Test {

    public static SObject getMetadata(SObjectType metadataSObjectType, Map<String, Object> fields) {
        Map<String, Object> jsonFields = new Map<String, Object> {
            'attributes' => new Map<String, Object> {
                'type' => metadataSObjectType.getDescribe().getName()
            }
        };
        if(fields != null) {
            jsonFields.putAll(fields);
        }
        return (SObject) JSON.deserialize(JSON.serialize(jsonFields), SObject.class);
    }

    public class Framework extends Framework {

        public override Type getType() {
            return Framework_Test.Framework.class;
        }

        override Framework_Metadata__c[] getAllMetadata() {
            return new Framework_Metadata__c[] {
                (Framework_Metadata__c) getMetadata(
                    Schema.Framework_Metadata__c.SObjectType,
                    new Map<String, Object> {
                        'Request__c' => SimpleRequest.class.getName()
                    }
                ), 
                (Framework_Metadata__c) getMetadata(
                    Schema.Framework_Metadata__c.SObjectType,
                    new Map<String, Object> {
                        'Request__c' => SpecialRequest.class.getName()
                    }
                )
            };
        }
    }

    @IsTest
    public static void testFramework() {
        // Data
        Framework_Test.Framework framework = new Framework_Test.Framework();

        Test.startTest();

        Result result = framework.processRequests();

        Test.stopTest();
    }
}
8

Take a look at the Custom Metadata Types Implementation Guide on page 14.

Use SOQL to access your custom metadata types and to retrieve the API names of the records on those types. DML operations aren’t allowed on custom metadata in Apex, the Partner APIs, and Enterprise APIs.

0

Not sure if you still need it, but I've created a small framework that allows you to mock any data you want. https://github.com/conductorsha/RecordRetriever

In your class you need to get records using:

RecordRetriever.getRecords('dataApiName', [SELECT ..... FROM .... WHERE ...]);

And in test class, before you call the method from the class, you need to mock the records you want:

Test Class

{
  ....
  List<....> mockedRecords = new List<...>{new ....(....)};
  RecrodRetriever.mockRecords('dataApiName', mockedRecords);
  .....call your business logic.....
}
0

I found a way to mock the error, using a static variable as a database, I can load a fake custom metadata record from the actual metadata, but with specific values for the test I need to do.

@IsTest
public class Object_1TriggerTest {
    @IsTest
    public static void Load_field_1_from_metadata(){
        //Initialization and data loading
        Object_1__c instance_of_object_1 = new Object_1__c(
            Name = 'Test',
            Picklist_1__c = 'picklist value 1'
        );

        MockMetadataUtils.insert_custom_metadata_mock();

        //Test
        Test.startTest();
        insert instance_of_object_1;
        Test.stopTest();
        //ASSERTS
        instance_of_object_1 = [
            SELECT ID,Field_1__c 
            FROM Object_1__c 
            WHERE ID = :instance_of_object_1.Id
        ];
        System.assertEquals('Value of metadata', instance_of_object_1.Field_1__c);
    }
}
public class MockMetadataUtils {
    
    public static List<example_metadata__mdt> rollupsumary = new List<example_metadata__mdt>();

    public static void insert_custom_metadata_mock(){
        example_metadata__mdt rollupsumary = new example_metadata__mdt();
        Map<String,Object> fields = (Map<String, Object>) 
            JSON.deserializeUntyped(JSON.serializePretty(rollupsumary));
        fields.put(
            example_metadata__mdt.Field_1__c.getDescribe().getName(),
            'picklist value 1'
        );
        fields.put(
            example_metadata__mdt.Field_2__c.getDescribe().getName(),
            'Value of metadata'
        );
        rollupsumary = (example_metadata__mdt) 
            JSON.deserialize(JSON.serialize(fields), example_metadata__mdt.class);
        MockMetadataUtils.rollupsumary.add(rollupsumary);
    }
}
public class Object_1TriggerHelper {
    public static void updateSomeField(List<Object_1__c> test_Objects){
        List<example_metadata__mdt> custom_metadata_records;
        Map<String,example_metadata__mdt> custom_metadata_records_map = new Map<String,example_metadata__mdt>();
        
        if(Test.isRunningTest()){
            custom_metadata_records = MockMetadataUtils.rollupsumary;
        }else{
            custom_metadata_records = example_metadata__mdt.getAll().values();
        }

        for(example_metadata__mdt custom_metadata : custom_metadata_records){
            custom_metadata_records_map.put(custom_metadata.Field_1__c, custom_metadata);
        }

        for(Object_1__c test_Object :test_Objects){
            test_Object.Field_1__c = custom_metadata_records_map.get(test_Object.Picklist_1__c) != null ? 
                custom_metadata_records_map.get(test_Object.Picklist_1__c).Field_2__c :
                null;
        }
    }
}

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.