3 ways to work with custom settings in an Apex unit test

Why can't you access your Salesforce org's custom setting data in a unit test?

You may be writing a unit test that involves a custom setting and expecting to be able to access your org’s data. But when you run the test, you find that there’s no data:

// Hmmm... why does this output an empty list?
System.debug(MyListSetting__c.getAll().values());

Explanation

The reason is that your org’s custom settings data is, by default, not accessible in tests. In this sense it behaves exactly like standard SObject records such as Account or Opportunity.

There are several options...

1. Create the custom setting data.

Insert the required custom setting data in your test just like any other test data. Either in the @TestSetup if the setting data is the same across all the tests in your test class, or in the individual tests themselves.

Inserting List Custom Settings with Apex

StateSetting__c california = new StateSetting();
california.Name = 'CA';
california.State__c = 'California';
insert california;

Inserting Hierarchy Custom Settings with Apex

Hierarchy custom settings have three levels of specificity - Organization default, profile-specific, and user-specific. You can assign the appropriate ID the the setting’s SetupOwnerId property:

Inserting an org default hierarchy custom setting
Discount__c orgDefaultDisc = new Discount__c(
    SetupOwnerId=UserInfo.getOrganizationId(),
    Discount__c=0.05
);
insert orgDefaultDisc;
Inserting a profile-level hierarchy custom setting
Profile salesPersonProfile = [
    Select Id From Profile
    Where Name = 'Sales Person'
];
Discount__c salesPersonDisc = new Discount__c(
    SetupOwnerId=profile.Id,
    Discount__c=0.1
);
insert salesPersonDisc;
Inserting a user-level hierarchy custom setting
// Create appropriate user
User vpOfSales = new User...
Discount__c vpOfSalesDisc = new Discount__c(
    SetupOwnerId=vpOfSales.Id,
    Discount__c=0.2
);
insert vpOfSalesDisc;

2. Access the custom setting data via a service and mock the service response when testing.

Another option is to enforce all custom data access via a service (i.e. via methods in an Apex class). For example, in the code below all access to the Discount__c custom setting is via methods in the DiscountService.

// Class encapsulating custom setting data access.
public class DiscountService {
	
    public Decimal getOrgDefaultDiscount() {
        return Discount__c.getOrgDefaults().Discount__c;
    }

    public Decimal getDiscount(ID userOrProfileID) {
        return Discount__c.getInstance(userOrProfileID).Discount__c;
    }
}

As an example of why this can be useful, the InvoiceService, shown below, injects the DiscountService via the constructor. This, crucially, allows a mock version to be passed in during a test. Mocks allow a response to be preprogrammed with the desired value for your custom setting.

public class InvoiceService {

    public DiscountService discService;
    // Dependency Injection allows the injection
    // of a mock discount service in tests.
    public InvoiceService(DiscountService ds) {
        this.discService = ds;
    }

    public Decimal calcNetTotal(Decimal gross){
        // Discount service used to obtain hierarchy
        // custom setting value for current user.
        // ***The getDiscount() response can be preprogrammed
        // to return your desired custom setting value.***
        Decimal disc =
            discService.getDiscount(UserInfo.getUserID);
        return gross * (1 - (disc/100));
    }
}

No records are inserted into the database during the test. Rather the DiscountService.getDiscount() method is stubbed using the fflib_ApexMocks library and preprogrammed to return a value when it is called during a test.

@isTest
static void calcNetTotal_Test() {
	
    // Setup
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    DiscountService mockDS =
        (DiscountService)mocks.mock(DiscountService.class);
	
    mocks.startStubbing();
    mocks.when(ds.getDiscount(fflib_Match.anyId())
         .thenReturn(30);
    mocks.stopStubbing();

    // Test
    // Uses the mock discount service which has
    // been set up to return 30 no matter
    // the user.
    InvoiceService invServ = new InvoiceService(mockDS);
    Decimal net = invServ.calcNetTotal(1000);

    // Assert
    System.assertEquals(700, net);
}

Of course that looks like a fair amount of setup as well, but this can be put into its own mock creation utility method for reuse:

private DiscountService createMockDiscountService(Decimal discount) {
    fflib_ApexMocks mocks = new fflib_ApexMocks();
    DiscountService mockDS =
        (DiscountService)mocks.mock(DiscountService.class);
	
    mocks.startStubbing();
    mocks.when(ds.getDiscount(fflib_Match.anyId())
         .thenReturn(discount);
    mocks.stopStubbing();

    return mockDS;
}

3. Use @isTest(SeeAllData=true) (but actually don’t!).

Using the @isTest(SeeAllData=true) annotation for a test class or test method will make your existing org custom setting data available during the test.

This may mean that you don’t have to setup any data or mock responses yourself, but it should be avoided as it comes with some pretty big downsides. Namely:

  • Changes to the org custom setting data (for instance if someone increases the sales person’s discount) may mean that tests that were previously passing start failing.
  • As well as custom setting data, @isTest(SeeAllData=true) makes all of the SObject data in the org visible to your tests as well, which may seriously affect your test results.