1

I have some code which relies on some Custom Metadata records to exist, but I don't want my tests to depend on real records in the org. How can I either inject or mock custom metadata records into the class?

Here's a psudocode example of what the code is doing:

global static String fetchCalloutPath(String endpointName){
        endpoint__mdt endpointMDT = endpoint__mdt.getInstance(endpointName);
        if(endpointMDT == null){
            throw new NoEndpointMetadataFoundException('Unable to find a endpoint__mdt record for endpoint "' + endpointName + '"');
        }
        Credential_Map__mdt credentialMDT = getCredential(endpointMDT.APIName__c);

        String calloutPath = 'callout:' + credentialMDT.Named_Credential__c + endpointMDT.Value__c;
        return calloutPath;
}

private static Credential_Map__mdt getCredential(String apiName){
    return [
        SELECT Named_Credential__c
        FROM Credential_Map__mdt
        WHERE APIName__c = :apiName
    ];
}

So in order to get a Credential_Map__mdt record I must first get an endpoint__mdt record.

I want to be able to reliably test and therefore mock or inject the metadata records, but I'm having a hard time understanding how best to achieve this, especially with there being a getInstance call involved. I'm thinking I might have to change that to a SOQL query in order to actually get this working but I'm hoping someone might have a solution for this (if so I can use this method elsewhere in the org). Also if I have to create interfaces or mock classes, will I need 2 due to the different metadata types involved? I'm hoping there might be some generic solution I could use elsewhere in the org.

Hoping for some guidance. Thanks!

3
  • The solution that I've used here is to make individual proxy classes for each MDT (though I imagine you could make it more abstract and return an Object that you would need to cast). salesforce.stackexchange.com/questions/385299/… . (The question would be closed if I were to suggest this question as a duplicate of that one).
    – Derek F
    Commented May 24, 2023 at 15:34
  • I did see that post, but as you say you need a class for each mdt. I don't think there's any way I can maintain the usability of getInstance with a more abstract solution since you seem to need a concrete metadata type to call it. I could rewrite them as SOQL if needs be; would've just been nice to still be able to use the standard methods provided
    – ministe
    Commented May 25, 2023 at 8:29
  • I'm w/ Derek - I use selector classes (fflib Selector pattern) to mock my MDT queries - this lets me use Dependency Injection to provide mock values in unit tests (for fflib, this is via ApexMocks)
    – cropredy
    Commented May 25, 2023 at 19:38

1 Answer 1

1

Dependency Injection is dead simple, and query injection is a great example of how to generalize that approach across your codebase. It's as simple as writing the following class.

public inherited sharing class Query
{
    public static List<SObject> records(List<SObject> records)
    {
        return instance.passThrough(records);
    }
    
    @TestVisible static void setMock(Service mock) { instance = mock; }
    static Service instance = new Service();
    public virtual class Service
    {
        protected List<SObject> passThrough(List<SObject> records)
        {
            return records;
        }
    }
}

Now anywhere you would query your metadata, wrap that call in Query.records.

public with sharing class MyService
{
    public static void myMethod(/*parameters*/)
    {
        List<My_Metadata__mdt> metadata = Query.records([
            SELECT ... FROM My_Metadata__mdt WHERE ...
        ]);
    }
}

The live implementation only needs to change very slightly, as above. It's a very safe change to make everywhere, as this logic behaves exactly the same except for in your tests. Just make sure you keep the API Version up to date. Your test can then setMock with a custom instance you set your own data store on.

@IsTest
class MyServiceTest
{
    @IsTest static void testMyMethod()
    {
        List<My_Metadata__mdt> metadata = new List<My_Metadata>();
        // populate as desired
        Query.setMock(new QueryMock(metadata));
        
        Test.startTest();
            MyService.myMethod();
        Test.stopTest();
        
        // assertions
    }
    class QueryMock extends Query.Service
    {
        final List<SObject> records;
        QueryMock(List<SObject> records)
        {
            this.records = records;
        }
        protected override List<SObject> passThrough(List<SObject> toIgnore)
        {
            return records;
        }
    }
}

I would make the QueryMock a public test utility, since you will have a large number of identical implementations where the only difference is the records parameter, which you pass in.

You can take this example and adapt it to be much more specific to your situation, rather than writing a library available for your whole codebase. Just make the inner class a part of your current top level service instead.

6
  • I was hoping to somehow be able to still use the getInstance methods but since they need a concrete type I don't think it'll be possible. They're just nice since the one method is overloaded so you can retrieve based on three different types of param. I could reproduce the effect with SOQL I suppose
    – ministe
    Commented May 25, 2023 at 9:47
  • You would be able to do Query.records(My_Metadata__mdt.getAll()). It does work. And this injection pattern itself could very easily be adapted to retrieve a single record, which I would put within your current service rather than as a generalization. If you understand the pattern, it is incredibly flexible and powerful.
    – Adrian Larson
    Commented May 25, 2023 at 14:06
  • Good point; I guess I wanted the query work to be done on the inside of the injector class but there's no need to do that. Final question then, in my example I have two queries in one function call. fetchCalloutPath has a query, then calls getCredential which has a query. How do I handle this? If I simply do Query.setMock before running a test of fetchCalloutPath, it'll return the same values for both. In general then, how do I mock for code which has more than one query?
    – ministe
    Commented May 25, 2023 at 14:26
  • Yes, you will sometimes need to write more sophisticated mocks. :) We have a couple different ways to go about that which I probably can't share (less generic IP) but the simpler approach is Map<SObjectType, List<SObject> that returns records based on the SObjectType being queried. A more complicated mock might be needed to mock multiple queries against a single table, where you maintain a List<List<SObject>> of the ordered results.
    – Adrian Larson
    Commented May 25, 2023 at 14:40
  • Thanks. That's unfortunate, feels like it takes away some of the beauty of mocking because it adds in some sort of reliance on the class being tested not changing it's queries too much. It's a shame there's not a more elegant solution. Appreciate your input
    – ministe
    Commented May 25, 2023 at 14:47

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .