21

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?

22

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.

  • 1
    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 :) – Rohit Kunal Dec 20 '18 at 11:24
  • 2
    I don't disagree :) – Phil Hawthorn Dec 20 '18 at 15:07
14

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

}
7

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();
    }
}
5

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.

4

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.

  • 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. – Sander de Jong Jan 8 at 8:27
  • @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 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. – Sander de Jong Jan 9 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 at 13:39

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.