1

Today I found out something weird about the way static variables act when used while running a test method and I wanted to know if anyone knows why this is the case.
I have a piece of code that creates a log record once an Incident record is created, and once when its status field value is changed to 'Closed'. In order to avoid redundancy I have a static boolean as a flag for the logic to run only once per context. Now, since incident creation and status change to 'Close' always happen on separate context in real life, this wasn't a problem. However, once I tried to write a test method to test my logic I realized that my static boolean flag doesn't reset even though using both test.startTest() and System.runAs(User) to start a new context. Eventually I went with this solution of resetting the flag's value manually. But I'm afraid I'm losing the ability to actually keep track of the flag behavior by doing so.

Here is my test method:

@IsTest
static void setClosingIncidentsStateTrackers(){
    //Select Required Records
    User                                incidentClient                  =   [SELECT Id FROM User WHERE Username = '[email protected]' LIMIT 1];
    BMCServiceDesk__Category__c         category                        =   [SELECT Id FROM BMCServiceDesk__Category__c WHERE Name = 'TestCategory'];
    BMCServiceDesk__BMC_BaseElement__c  service                         =   [SELECT Id FROM BMCServiceDesk__BMC_BaseElement__c WHERE Name = 'TestService'];
    BMCServiceDesk__BMC_BaseElement__c  serviceOffering                 =   [SELECT Id FROM BMCServiceDesk__BMC_BaseElement__c WHERE Name = 'TestServiceOffering'];
    BMCServiceDesk__Urgency__c          highUrgency                     =   [SELECT Id FROM BMCServiceDesk__Urgency__c WHERE Name = 'גבוה'];
    BMCServiceDesk__Impact__c           siteImpact                      =   [SELECT Id FROM BMCServiceDesk__Impact__c WHERE Name = 'אתר'];
    Problem_Source__c                   problemSource                   =   [SELECT Id FROM Problem_Source__c WHERE Name = 'בעיית הגדרות'];

    List<BMCServiceDesk__Incident__c> incidents = new List<BMCServiceDesk__Incident__c>();
    for(integer i = 0; i < batchSize; i++){
        BMCServiceDesk__Incident__c incident = new BMCServiceDesk__Incident__c(
            BMCServiceDesk__FKClient__c             =   incidentClient.ID,
            BMCServiceDesk__FKCategory__c           =   category.ID,
            BMCServiceDesk__FKServiceOffering__c    =   serviceOffering.ID,
            BMCServiceDesk__FKBusinessService__c    =   service.ID,
            BMCServiceDesk__FKStatus__c             =   awaiting_for_handling
        );
        incidents.add(incident);
    }
    insert incidents;

    for(BMCServiceDesk__Incident__c incident : incidents){
        incident.BMCServiceDesk__FKStatus__c = in_work;
    }       
    update incidents;

    test.startTest();
    System.runAs(new User(Id = UserInfo.getUserId())){
        for(BMCServiceDesk__Incident__c incident : incidents){
            incident.BMCServiceDesk__FKStatus__c    = handled;
            incident.BMCServiceDesk__FKBMC_BaseElement__c = service.ID;
            incident.BMCServiceDesk__ClosureCategory__c = 'נפתר';
            incident.BMCServiceDesk__incidentResolution__c = 'נפתר';
            incident.BMCServiceDesk__state__c       = false;
        }
        System.debug(LoggingLevel.ERROR, '*** /// Closing Incidents /// ***');
        //MNR_Incident_TriggerHandler.incidentTrackersWereAlreadyCreatedInThisContext = false; <-- This is the manual reset of the static boolean flag
        update incidents;
    }
    test.stopTest();

    List<Incident_State_Tracker__c> ists = [SELECT Id, Incident__c, State__c FROM Incident_State_Tracker__c];

    System.debug(LoggingLevel.ERROR, '*** // ' + ists.size() + ' // ***');
}

I'd like to know if you guys encounter this issue and know how to force clean new context inside a test method for resetting static variables.

Thank you very much in advance,
Lior.

2
  • Why don't you just reset the static variable from your own test class, right after the insert dml? Commented May 17, 2020 at 18:18
  • @SergioAlcocer - As mentioned in the question: "Eventually I went with this solution of resetting the flag's value manually. But I'm afraid I'm losing the ability to actually keep track of the flag behavior by doing so."
    – Lior Lavi
    Commented May 18, 2020 at 4:17

1 Answer 1

4

I think there are two linked misunderstandings here.

I realized that my static boolean flag doesn't reset even though using both test.startTest() and System.runAs(User) to start a new context.

Test.startTest() and System.runAs() both manipulate aspects of the transaction context: Test.startTest() resets your governor limits, and System.runAs() alters the running user. Crucially, however, neither starts a new transaction. And static variables are scoped to the transaction.

When running unit tests, each test method executes in a single transaction. You can't start a new transaction inside a unit test. That's part of why you'll sometimes see unusual behavior in Asynchronous Apex when it's forced to execute inside a unit test method, synchronously, with Test.stopTest(): it's executing in a "dirty" transaction context, instead of its own isolated transaction as it expects. (This is the culprit behind many CalloutExceptions when running tests).

The other is

In order to avoid redundancy I have a static boolean as a flag for the logic to run only once per context.

This is almost never what you really want. You certainly want to ensure your code functions correctly when recursion occurs, but you don't want your whole trigger, or even one trigger event, to run exactly once per transaction. Among other subtle and difficult-to-debug maladies, this causes your trigger to silently fail if more than 200 records are touched in a transaction, or if any number of records (even 1) are both inserted and subsequently updated in a single transaction.

An answer by sfdcfox nicely covers a better recursion-guard pattern. In other circumstances, you may need to alter trigger logic to take no action when not required by state (best), or to track records already affected via a Set<Id>, rather than a static Boolean.

3
  • 1
    On the last point, I had this answer.
    – Phil W
    Commented May 17, 2020 at 19:33
  • Hi David, thank you for reply. The reason I figured test.start() might reset the context of the code is because it also resets its limits. As for the use of System.runAs(), I tried it because I leared it can be used to avoid the Mixed Standard and Non-Standard Object Insert DML error, and I wondered if it is doing so by running in a seperate context. As to the boolean for recursion avoidens issue, I'll look into best practices on the matter as for your comment and Phil's. By the way, can you provide reference from the documentation to the inability to start new context in test methods?
    – Lior Lavi
    Commented May 18, 2020 at 4:53
  • 1
    I'm not sure that's explicitly documented. The docs don't cover the transaction structure of unit tests in any great detail. However, there's no way to start a new transaction explicitly in Apex at all, and the normal strategy for doing so - using Asynchronous Apex - does not apply in a unit test context.
    – David Reed
    Commented May 18, 2020 at 14:31

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .