4

It's been fairly well established1, 2 that inserting/updating custom settings in a unit test hurts several aspects of the test:

  • Makes tests take longer to execute
  • Makes it hard to run tests in parallel
  • Part of the hit on parallelism is the UNABLE_TO_LOCK_ROW errors that become fairly common

With that in mind, I'm trying to put together a generic proxy class (part of a larger, personal project) to substitute direct calls to custom settings like MyCustomSetting__c.getInstance() with something like CustomSettingProxy.find('MyCustomSetting__c').getInstance().

Having the proxy class handle access and modification would allow us to set up custom setting instances in tests without the need for DML.

Handling the test portion is relatively simple, load data in the test method itself, and then it's available in the code being tested. The bit I'm stuck on is in normal operation.
In normal operation, we won't have data pre-loaded. We can pass the custom setting name (or the SObjectType, or the .class). If we see (in the proxy class) that there is no pre-loaded data for the custom setting, how can we attempt to grab that data?

A few possible solutions I've come up with include (in no particular order):

  1. Figuring out what the parent type of any given Custom Setting is, so that we can call parentType.getInstance() (static methods). Documentation suggests this might be CustomSetting__c, but anonymous apex shows that this is not a valid type. A Custom Setting is a subclass of SObject, but SObject has no static methods (only instance methods)
  2. Make a dynamic query using the information we do have (which burns a query for every custom setting that is accessed, which is... not so desirable)
  3. Create and maintain separate proxy classes for each custom setting (if we know (statically) which custom setting we're being asked for, we can simply use <MyCustomSettingName>.getInstance()). I'm hoping there's a way to avoid this and just have a single, more abstract class
  4. Have clients call a method to pass in available data, e.g. CustomSettingProxy.load(MyCustomSetting__c.getInstance());, before calling CustomSettingProxy.find('MyCustomSetting__c').getInstance();. It'd work, but I imagine it'd be a hard sell (why do I need to call getInstance() twice?)

I'd like to avoid queries, avoid dml, and avoid making more than one class. I'm looking for a way to access the application cache (which is different from the platform cache) dynamically. Alternatively, adding custom setting data to the application cache without using DML would get rid of the need for a proxy in the first place.

Is there a way to do this, or do I need to settle on one of the solutions I've already come up with?

1: https://force201.wordpress.com/2019/05/14/embracing-apex-parallel-testing/
2: http://www.fishofprey.com/2018/06/speeding-up-salesforce-unit-testing.html

3
  • 1
    option 3 with a big switch statement for each known custom setting but I'm betting sfdcfox will come up with a better answer
    – cropredy
    Oct 18 '20 at 17:47
  • Added an idea for this, though I expect it will sit in limbo for at least a decade (if it ever gains the attention of a product manager).
    – Derek F
    Oct 18 '20 at 20:33
  • Note that it takes some work to enable new custom list settings as SFDC wants us to use custom metadata.
    – cropredy
    Oct 18 '20 at 21:06
2

You can use a class to handle the access to the custom settings and have it implement a custom logic using Test.isRunningTest(). The idea would be that when not running a test, it would return the correct type of the custom setting the caller wants to access. When running a test though, it could search for the value in a private, test visible, map of values.

private static Map<String, Object> values;

public static Object getValue(String csName) {
    if (Test.isRunningTest()) {
        return values.get(csName);
    }
    return CustomSetting__c.getInstance().Value__c;
}

In this scenario, in your test, you would set the values of values before executing the unit test:

CustomSettingProxy.values.put('DoTheThing__c', true);

Test.startTest();

// ... it should do the thing when the code tries to access
// the custom setting value

Test.stopTest();

Another approach, elaborating a little more on the use of Test.isRunningTest() method, is to create a class that has a mapping to the implementation of a class that handles the call to the custom setting. This way you would be effectively creating a class for each custom setting, implementing a common interface. In this approach the proxy class has a mapping to which implementation it should call using Type.forName, for example.

Then you would have each custom setting pointing to each respective class implementing such interface to get the values from the SObject. The same principle of substitution applies here: before the unit test is actually run you would replace the mapping with a mock class (which I believe could be a subclass of your test class).

These solutions look more like the third approach mentioned: Create and maintain separate proxy classes for each custom setting.

4
  • 3
    This doesn't really resolve the original problem, though, you still need static-linked sObject. It's unfortunate we don't have a parent type like sObject for all custom settings to allow more dynamic calling.
    – sfdcfox
    Oct 18 '20 at 18:12
  • 3
    I agree that this looks like solution #3. If I take a step back and think about my overall project (another trigger framework to add to the list of frameworks we already have), #3 doesn't sound so bad as I'm only planning on using a single custom setting at this point. Solution #1 would be my preference, but it sounds like that'd need to be pitched through Trailblazer ideas (and therefore spend a decade in limbo).
    – Derek F
    Oct 18 '20 at 18:33
  • @sfdcfox that's where using a common interface would help. It would be necessary to write (or more like copy and paste) some code per custom setting. But that would be it. The proxy class just gets the name and handles everything. I like the flexibility this one gives. :) Oct 18 '20 at 19:01
  • 1
    It was hard for me to choose between this answer and Phil W's answer, but in the end I think this approach is just a little bit more flexible. If someone has a list custom setting (older org, or specifically enabled it through the setup menu), .getAll(), .getInstance(), and .getValues() would return null (whereas hierarchy custom settings return a blank record at worst) . This approach can avoid a null check (at the cost of having an @testVisible variable). It's the smallest of differences, but I personally prefer @testVisible over an extra null check.
    – Derek F
    Oct 24 '20 at 13:35
2

Our solution was to introduce a "settings repository" class. This didn't try to be "all things to all men" and instead exposed custom settings relevant to the app. I'm not even sure it makes sense to try to do that anyway as an app will have certain expectations for the various custom settings types it uses.

The class's job is to in-memory cache specific settings instances. Doing this means that, as long as all code (production and unit tests) use this "settings repository" class, unit tests can invoke the class to get the required settings instance and set the fields it wants to set. As long as this happens before the tested production code looks at the field values, the tested code sees the correct settings data.

We don't even care that the unit tests will actually load the in-org settings data because the unit tests are written to explicitly set those fields needed by the tested code. This means the "settings repository" simply invokes the normal custom settings method, but only if it doesn't already have a cached value.

Our repository class has type-specific custom settings methods that support the settings in a manner appropriate to how we expect them to be used (e.g. for some hierarchy settings we get the user-specific instance but for others we always use the org default since we don't want it user-specific).

For example:

public class Settings {
    private static ExampleSettings__c example = null;

    public ExampleSettings__c getExample() {
        if (example == null) {
            example = ExampleSettings__c.getInstance();
        }

        return example;
    }
}

Each time we have another type of settings we add another method for accessing it in an app-appropriate manner.

Production code simply accesses the settings as needed, e.g.:

if (Settings.getExample().SomeFlag__c) {
    // Do something appropriate
}

A unit test can include methods that test the settings-based scenarios very simply too:

Settings.getExample().SomeFlag__c = false;

// Now call the production code that uses SomeFlag__c. Of course, if there are
// multiple fields to be set the ExampleSettings__c instance can be held in a
// local variable for improved readability

Or:

Settings.getExample().SomeFlag__c = true;

// Again, call the production code

This is a cheap way of addressing the production vs test case with minimal overhead and still leveraging the underlying custom settings caching that Salesforce provides.

I know this doesn't do quite what you discussed, but like I said - it makes sense for the app code to constrain how settings are accessed across the entire code-base.

3
  • ah, I was hoping you would chime in, as I saw several references to an "access class" for custom settings in my searching.
    – Derek F
    Oct 18 '20 at 21:10
  • I have provided a much cut-down version of what we actually do, since we also handle full defaulting of fields in our custom settings - those fields that have a defined default in the custom settings type that are then set null in the in-org instance(s) will receive the default value. We do that by getting the default instance using ExampleSettings__c.getSObjectType().newSObject(null, true) then merge in the values during the initial fetch of the underlying settings and before the result gets in-memory cached...
    – Phil W
    Oct 18 '20 at 21:16
  • Anyway, I hope the answer is of some help.
    – Phil W
    Oct 18 '20 at 21:18

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.