[SalesForce] Custom Setting is null in test method even though I set a value

I want to update the Opportunity stage depending on a status field on the account. I have a validation rule that makes a date field on the opportunity mandatory for some stages. This validation rule should not apply to my update of the opportunity in the trigger, so I added a custom setting (type: hierarchy) to the validation rule and change its value in the code before and after updating the opportunity. I use a hierarchy setting as proposed here: https://salesforce.stackexchange.com/a/10353.

Here is the validation rule:

AND(
                NOT($Setup.ValidationRuleControl__c.Skip__c),
                ISNULL(ExpectedLiveDate__c), 
                OR(
                               ISPICKVAL(StageName,"Negotiation Started"), 
                               ISPICKVAL(StageName,"Negotiation Advanced"), 
                               ISPICKVAL(StageName,"Onboarding Started")
                ) 
)

If ValidationRuleControl__c.Skip__c is true, the ExpectedLiveDate__c is not mandatory.

Here is the piece of code that does the update of the opportunity:

    //turn off validation rules where the custom setting ValidationRuleControl__c is used
    public static void turnOffValidationRules() {
                ValidationRuleControl__c tempValidationRuleControl = ValidationRuleControl__c.getInstance();
                               if(tempValidationRuleControl == null) 
                                               tempValidationRuleControl = new ValidationRuleControl__c();
                               tempValidationRuleControl.Skip__c = true;
                               upsert tempValidationRuleControl;
    }

    //turn on validation rules again (after DML)
    public static void turnOnValidationRules() {
                               ValidationRuleControl__c tempValidationRuleControl = ValidationRuleControl__c.getInstance();
                               if(tempValidationRuleControl == null) 
                                               tempValidationRuleControl = new ValidationRuleControl__c();
                               tempValidationRuleControl.Skip__c = false;
                               upsert tempValidationRuleControl;
    }

                //updates a list of SObjects + error handling
    public static void updateSObjects(List<SObject> obsList, String errorMailSubject, String ErrorMailBody) {
                if (obsList != null)
                               if (!obsList.isEmpty()) {
                                               turnOffValidationRules();
                                               System.debug('>>>>>> ValidationRuleControl: ' + ValidationRuleControl__c.getInstance());
                                               //try to update all SObjects
                                                               [...]
                                               turnOnValidationRules();
                                               System.debug('>>>>>> ValidationRuleControl after turning on again: ' + ValidationRuleControl__c.getInstance());
                                               }
                                }
   }

This works fine when I test the function manually in the browser. But when I run my unit test, it fails because the validation rule prevents the opportunity update. I do set the custom setting ValidationRuleControl__c.Skip__c in my unit test to false to simulate the data. But at the moment of the update, ValidationRuleControl__c.Skip__c is actually null:

20:31:55.568 (6568908000)|VALIDATION_FORMULA|AND(
NOT($Setup.ValidationRuleControl__c.Skip__c),
ISNULL(ExpectedLiveDate__c), 
OR(
   ISPICKVAL(StageName,"Negotiation Started"), 
   ISPICKVAL(StageName,"Negotiation Advanced"), 
   ISPICKVAL(StageName,"Onboarding Started")
  ) 
)|StageName=Onboarding Started , $Setup.ValidationRuleControl__c.Skip__c=null , ExpectedLiveDate__c=null
20:31:55.568 (6568923000)|VALIDATION_FAIL
[...]
20:31:55.574 (6574533000)|USER_DEBUG|[166]|DEBUG|FIELD_CUSTOM_VALIDATION_EXCEPTION - Please enter an Expected Live Date

It looks like I don't set the custom setting in the right way.
Why is ValidationRuleControl__c.Skip__c null?

I set ValidationRuleControl__c.Skip__c, then call Database.update(), which causes the beforeUpdate trigger to fire. Then the custom validation rules are checked. The strange thing here is that my trigger has finished before the validation rules are checked, so the context is lost. (Which actually fits with the order of execution.) How do I work around that? I don't want to use IsTest(SeeAllData=true).

Edit: Here is the test method as requested:

public static void setupCustomSettings() {
    [...]
    //Set up Validation Rule Control as default (false)
    ValidationRuleControl__c tempValidationRuleControl = new ValidationRuleControl__c();
    tempValidationRuleControl.Skip__c = false;
    insert tempValidationRuleControl;
}

//checks that the opportunity stage is changed to "Onboarding Started" when the current account status is set to onboarding
//also checks that this works even if the expected live date is missing
static testMethod void testOpportunityUpdatingForOnboarding() {
TriggerHelper.setupCustomSettings();

//create a merchant account
Account merchant = new Account(RecordTypeId = TriggerHelper.getRecordTypeId(Account.SObjectType, TriggerHelper.rtName.Merchant),
                                Name = 'Test Merchant',
                                CurrentAccountStatus__c = Label.CurrentAccountStatus_New,
                                ShippingCountryCode = 'AT',
                                SubChannel__c = 'Skill Games');
insert merchant;

//create an oportunity
Opportunity opp = new Opportunity(AccountId = merchant.Id,
                                  RecordTypeId = TriggerHelper.getRecordTypeId(Opportunity.SObjectType, TriggerHelper.rtName.Opportunity_new),
                                  Topic__c = Label.OpportunityTopic_FirstContract,
                                  StageName = Label.OpportunityStage_New,
                                  Priority__c = Label.OpportunityPriority_Medium,
                                  Name = 'Test Merchant',
                                  CloseDate = Date.today() //mandatory field
                                  //works without expected live date because the validation rule is skipped 
                                  //see the custom setting ValidationRuleControl__c
                                  //ExpectedLiveDate__c = Date.today() + 1 
                                  ); 
insert opp;

//start onboarding
Test.startTest();
merchant.CurrentAccountStatus__c = Label.CurrentAccountStatus_Onboarding;
update merchant;
Test.stopTest();

//check that the opportunity stage has changed
opp = [select Id, StageName from Opportunity where Id = :opp.Id];
System.assertEquals(Label.OpportunityStage_OnboardingStarted, opp.StageName);
}

Best Answer

I'm using the same pattern, successfully switching off validations when building complex data structures within unit tests. Your case intrigued even more, as your code looks absolutely correct. After a bit of investigation I've come to the conclusion that Salesforce debug log misleadingly displays values of Custom Settings set within unit tests as NULLs.

In other words, in your unit test, while evaluating the Validation Rule's condition, $Setup.ValidationRuleControl__c.Skip__c has in fact value false. You can easily test it by amending the unit test to set Skip__c to true - the validation rule should not stop the update.