Static Variables and Unit Tests

I am trying to write a class to cache SObjects accross Trigger contexts. I understand that static variables will not persist across separate unit tests but mine don't seem to persist in any test. And I am stuck figuring out where I am going wrong on this.

I am trying to practice TDD when I can and I am stuck at my first test. Here is the class so far. I have tried different methods of initializing the cacheMap property to see if that solved it.

public with sharing class CacheManager {

    @TestVisible
     private static Map<SObjectType, Map<Id, SObject>> cacheMap;

    public static void addToCache(Map<SObjectType, Map<Id, SObject>> cacheMap) {
        if(cacheMap == null){
            cacheMap = new Map<SObjectType, Map<Id, SObject>>()
        }
        cacheMap.putAll(cacheMap);
        System.debug(cacheMap);
    }

}

Below is a test

@IsTest
private class CacheManagerTest {
    @IsTest
    static void addToCache() {
        //given
        Id id = TestingUtils.generateId(Patient__c.SObjectType);
        Map<SObjectType, Map<Id, SObject>> testMap = new Map<SObjectType, Map<Id, SObject>>();
        Map<Id, Patient__c> patientMap = new Map<Id, Patient__c>{Id => new Patient__c()};
        testMap.put(Patient__c.SObjectType, patientMap);

        //when
        CacheManager.addToCache(testMap);

        //then
        System.debug(CacheManager.cacheMap);
        System.assertEquals(true, CacheManager.cacheMap.containsKey(Patient__c.SObjectType));

    }
}

When this runs I get a null reference on the assertion. When I step through it in the debugger or read the logs the cacheMap property is getting populated. But when the stack returns to the unit test the value reverts to null.

Answers 1

  • The problem is that you introduced Name Shadowing in your method. That is, the local parameter overrides the static variable, so you'd have to address it differently. This should have been obvious when you wrote:

    cacheMap.putAll(cacheMap);
    

    Here, you're adding the contents of the map back into itself.

    To avoid this problem, use different names (note: Apex is case insensitive, so differing by case won't help).

    In addition, be careful when you use putAll like that. If there are already records in the cache for a given sObjectType, they will be expunged, since you're literally replacing the entire map that was storing the data.

    Instead, you want to do something like this:

    @TestVisible
    private static Map<SObjectType, Map<Id, SObject>> cacheMap = new Map<SObjectType, Map<Id, SObject>>();
    
    public static void addToCache(Map<SObjectType, Map<Id, SObject>> recordsToAdd) {
        for(SObjectType sType: recordsToAdd.keySet()) {
            Map<Id, sObject> sObjectTypeCache = cacheMap.get(sType);
            if(sObjectTypeCache == null) {
                cacheMap.put(sType, new Map<Id, sObject>());
            }
            sObjectTypeCache.putAll(recordsToAdd.get(sType));
        }
    }
    

    Note that this is probably a bad implementation, as you're requiring the developer to specify a complex data structure. When I create my cache implementations, I generally have two structures, one map that's just all records by Id (Map<Id, SObject>), and one that keeps track of the Ids by sObjectType (Map<sObjectType, Set<Id>>). This will give you greater flexibility in being able to retrieve a record by Id without knowing its type, and allow you to retrieve all the records that match a given sObjectType, which both end up being useful functions in many cases.


Related Questions