2

I've written two Apex Triggers, one on the Opportunity object, and one on a custom object. These two triggers keep the two objects' checkboxes in sync; setting a value on one object's value sets the corresponding object's value to false.

I've written a very simple class to prevent an infinite loop where the triggers cause each other to fire. The class looks like this:

public class RecursiveTriggerHandler{
     public static Boolean isFirstTime = true;
}

Both Apex triggers start with the following lines of code:

if(RecursiveTriggerHandler.isFirstTime){
    RecursiveTriggerHandler.isFirstTime = false;

I have unit tests that check the triggers' behavior in both directions. The problem is: my tests are not covering one of the script's code past this line:

if(RecursiveTriggerHandler.isFirstTime){

This means the test thinks RecursiveTriggerHandler.isFirstTime is false when it starts running this test. I'm not sure why this would be the case, as I've put the tests in separate methods, in separate classes.

Here is the first test (:

    @isTest 
    static void testOpportunityCheckboxChangeTrue () {
        RecursiveTriggerHandler.isFirstTime = true;
        RecursiveTriggerHandler rescursive = new RecursiveTriggerHandler();
        Opportunity opp = new Opportunity(Name = 'MEDICC Test Opportunity', CloseDate = Date.valueOf(FIVE_DAYS_FROM_NOW), OwnerId = user.Id, Pain_identified__c = true, StageName = '0 - Qualification');
        insert opp;
        iseeit__Qualifier_Milestone__c qualifier = new iseeit__Qualifier_Milestone__c(iseeit__Opportunity__c = opp.Id);
        insert qualifier;
        iseeit__TO_DO__c todo = new iseeit__TO_DO__c(iseeit__title__c = 'Pain identified', iseeit__status__c = false, iseeit__Qualifier_Milestone__c = qualifier.id);
        insert todo;

        opp.Pain_identified__c = false;

        update opp;
        update todo;

        System.assertEquals(todo.iseeit__status__c, opp.Pain_identified__c);
        }    
}

And here is the second test (it's a part of a different class from the first test):

  @isTest 
    static void testOpportunityCheckboxChangeTrue () {
        Opportunity opp = new Opportunity(Name = 'MEDICC Test Opportunity', CloseDate = Date.valueOf(FIVE_DAYS_FROM_NOW), OwnerId = user.Id, Pain_identified__c = true, StageName = '0 - Qualification');
        insert opp;
        iseeit__Qualifier_Milestone__c qualifier = new iseeit__Qualifier_Milestone__c(iseeit__Opportunity__c = opp.Id);
        insert qualifier;
        iseeit__TO_DO__c todo = new iseeit__TO_DO__c(iseeit__title__c = 'Pain identified', iseeit__status__c = false, iseeit__Qualifier_Milestone__c = qualifier.id);
        insert todo;

        todo.iseeit__status__c = true;

        update todo;
        update opp;


        System.assertEquals(todo.iseeit__status__c, opp.Pain_identified__c);
        }

Can I somehow guarantee that RecursiveTriggerHandler.isFirstTime will be true at the start of each of these tests?

  • 3
    You shouldn't use Boolean flags for recursion blockers anyway. They will cause bugs. – Adrian Larson Apr 16 at 17:59
  • What type should I be using? Just a String? – snark17 Apr 16 at 18:48
  • 3
    There are safe recursion guard patterns; this just isn't one of them. sfdcfox wrote an answer a while back demonstrating (a) better pattern, where you carefully set the Boolean only around the potentially-recursive DML operation and then immediately clear it. – David Reed Apr 16 at 19:23
  • Thanks for the link! I'm a bit confused about the first pattern he points to; is he creating a recursion handling class, or integrating a recursion handling method directly in the code? – snark17 Apr 16 at 20:57
3

In a comment, I pointed you to this answer from sfdcfox, which is a nice explanation of how particular static Boolean-based recursion guards fail and can be made effective using a different pattern. I'll expand on that a little to apply it here.

You have two triggers, each of which makes changes to the object on which the other is defined. Your objective is to ensure that when Trigger A on Object 1 goes off and updates Object 2, Trigger B on Object 2 doesn't then recursively update Object 1.

The safe static Boolean pattern for this situation would look something like this, using Account and Opportunity as the sObjects involved.

public class AccountTriggerHandler {
    @TestVisible
    private static Boolean inhibit = false;

    public void beforeUpdate(List<Account> newList, Map<Id, Account> oldMap) {
        if (inhibit) return;

        // Do stuff... create a `List<Opportunity>` to update.

        // Set the flag to inhibit re-firing of *this* trigger before we update
        // the other object.
        inhibit = true;
        update opportunitiesToUpdate;
        // Reset the inhibit flag so that all other trigger action
        // completes normally.
        inhibit = false;
    }
}

Here, we set the inhibit flag only within the trigger handler itself, and only around the actual DML operation on the other object (Opportunity, here) that will result in a recursive update of our own object (Account, here). We don't set the flag at any other time, so that, for example, 400-record transactions will stilltri complete successfully with two trigger invocations, and we will not interfere with retries when DML is performed with allOrNone=false.

If we needed the Opportunity trigger to access the flag (I don't think we do here), it could be made public rather than @TestVisible private, which allows us to set it in test context and thereby validate its operation.

To save a little on limits, you might consider a similar design where the inhibitor flag is set on the other object:

    OpportunityTriggerHandler.inhibitAccountUpdates = true;
    update opportunitiesToUpdate;
    OpportunityTriggerHandler.inhibitAccountUpdates = false;

Then, your OpportunityTriggerHandler can avoid running SOQL or performing expensive processing it might execute in an attempt to update the Accounts that actually started this sequence of functionality.

  • I recommend adding a try {...} finally {...} to handle resetting inhibit to false in the handler, just in case the update for opportunities fails and causes an exception that itself doesn't prevent continued processing from the account updating code. – Phil W Apr 18 at 13:02

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.