mitchspano/apex-trigger-actions-framework

Bypassed MDT Causes Apex Tests to Fail

Closed this issue · 4 comments

Expected Behavior

When an sObject_Trigger_Setting__mdt or Trigger_Action__mdt has Bypass_Execution__c == TRUE, during execution of apex test class methods, TAF should execute the configured Trigger Action anyways.

Actual Behavior

Apex test methods that rely on TAF executing a configured trigger action fail when a global bypass is set in the MDT.

Steps to Reproduce the Problem

  1. run a known working apex test class method that relies on the TAF
  2. bypass either the corresponding sobject mdt or trigger action mdt
  3. run apex test class method again, witness failure

Specifications

  • Version: 0.2.8
  • Platform: Salesforce sandbox

There are two solution to this:

  1. Do not perform DML to test your trigger logic - see https://github.com/mitchspano/apex-trigger-actions-framework?tab=readme-ov-file#dml-less-trigger-testing

  2. Inject the Trigger_Action__mdt rows you want into execute into MetadataTriggerHandler during test execution:
    https://github.com/mitchspano/apex-trigger-actions-framework/blob/main/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls#L339

Generally speaking - this is not an issue with the framework, but a structural change which to the way unit tests are to be written in your org.

The system is working as intended, so I am closing this issue.

Those are both fair points.
I've been taking for granted that live custom metadata records are freely available in a test context.
I'll pivot to explicitly creating/updating the custom metadata records that my logic relies upon.

Thanks for the quick response!

I attempted to explicitly set the afterInsertActionMetadata per the second link you provided

   @TestSetup
    private static void setup() {
        sObject_Trigger_Setting__mdt sobjectMDT;
        try{
            sobjectMDT = [SELECT Bypass_Execution__c FROM sObject_Trigger_Setting__mdt WHERE Object_API_Name__c = 'ContentDocumentLink'];
            if(sobjectMDT.Bypass_Execution__c != false){
                sobjectMDT.Bypass_Execution__c = false;
            }
        } catch (Exception e){
            sobjectMDT = new sObject_Trigger_Setting__mdt(Object_API_Name__c='ContentDocumentLink', Bypass_Execution__c = false);
        }
        
        Trigger_Action__mdt triggerActionMDT;
        try{
            triggerActionMDT = [ SELECT Bypass_Execution__c FROM Trigger_Action__mdt WHERE Apex_Class_Name__c = 'TA_ContentDocumentLink_ReopenCase' AND After_Insert__c = :sobjectMDT.Id];
            if(triggerActionMDT.Bypass_Execution__c != false){
                triggerActionMDT.Bypass_Execution__c = false;
            }
        } catch (Exception e){
            triggerActionMDT = new Trigger_Action__mdt(Apex_Class_Name__c = 'TA_ContentDocumentLink_ReopenCase', After_Insert__c = sobjectMDT.Id, Bypass_Execution__c = false);
        }
        MetadataTriggerHandler.afterInsertActionMetadata = new List<Trigger_Action__mdt>{triggerActionMDT};
    }

But I got this deployment error Variable does not exist: afterInsertActionMetadata presumably because the set accessor doesn't have an explicit implementation.

I also tried the following, both in the setup and directly in the test method and still the action is bypassed.

    private static void setup() {
        MetadataTriggerHandler.clearBypass('ContentDocumentLink');
        MetadataTriggerHandler.clearBypass('ReopenCaseUponFileUpload');
    }

I must not entirely grok your second point above.
What is the recommended approach to inject these MDT records?

This can be accomplished without any modifications to the framework by leveraging the fact that the inner MetadataTriggerHandler.Selector is @TestVisible and virtual:

@TestVisible
private virtual inherited sharing class Selector {
  public virtual List<SObject> query(String queryString) {
    return Database.query(queryString);
  }
}

So in a test class, you can override this and mutate the query results however you like:

@IsTest
private class SomeTest {

  @IsTest
  private static void fooShouldBar() {
    MetadataTriggerHandler.selector = new SomeTest.FakeSelector();
  
    insert new Account(Name = 'Acme'); // _all_ trigger actions will execute now, regardless of `Bypass_Execution__c` value
  }

  private class FakeSelector extends MetadataTriggerHandler.Selector {
    public override List<SObject> query(String queryString) {
      String queryStringWithoutBypassCheck = queryString.replace('AND Bypass_Execution__c = FALSE', '');
      List<SObject> allTriggerActionsForThisContext = Database.query(queryStringWithoutBypassCheck);
      // mutate further if you like - perhaps setting `Bypass_Permission__c` or `Required_Permission__c` as needed
      return allTriggerActionsForThisContext;
    }
  }
}