1

I'm running into a somewhat bizarre situation when trying to run unit tests for my apex code. The functionality is that when a contact is created, a child record of a custom object is also created and values from contact fields are written over to the child object. Then, when a particular contact field changes, the code should update the previously created child record with some new information (including the new value of the contact field) and then create an additional new child record, effectively creating snapshot records.

This works fine when testing it in sandbox. I create a record, a child record is created. I update the contact by changing the field, it updates the first child record and creates a new one. I can't seem to figure out the error I'm getting when writing a test class. The first record is created, but it is not getting updated, nor is the second record being created.

trigger:

trigger ContactTrigger on Contact (before insert,before update, after insert, after update, before delete, after delete) {
if(Trigger.isInsert && Trigger.isBefore){
    ContactTriggerHandler.handleBeforeInsert(Trigger.new);
}
else if(Trigger.isInsert && Trigger.isAfter){
    ContactTriggerHandler.handleAfterInsert(Trigger.new,Trigger.newMap,Trigger.oldMap);
}
else if(Trigger.isUpdate && Trigger.isBefore){
    ContactTriggerHandler.handleBeforeUpdate(Trigger.old,Trigger.new,Trigger.newMap,Trigger.oldMap);
}
else if(Trigger.isUpdate && Trigger.isAfter){
    ContactTriggerHandler.handleAfterUpdate(Trigger.old,Trigger.new,Trigger.newMap,Trigger.oldMap);
}
else if(Trigger.isDelete && Trigger.isBefore){
    ContactTriggerHandler.handleBeforeDelete(Trigger.old,Trigger.new,Trigger.newMap,Trigger.oldMap);
}
else if(Trigger.isDelete && Trigger.isAfter){
    ContactTriggerHandler.handleAfterDelete(Trigger.old,Trigger.new,Trigger.newMap,Trigger.oldMap);
}
else if(Trigger.isUndelete && Trigger.isBefore){
    ContactTriggerHandler.handleBeforeUndelete(Trigger.old,Trigger.new,Trigger.newMap,Trigger.oldMap);
}
else{}
}

Simplified Handler Class:

public class ContactTriggerHandler {

    /********************************************************
     * 
     * Private Variable
     * 
     * Prevent recursion
     * 
     * *****************************************************/

    private static Map<Id,Boolean> alreadyProcessed = new Map<Id,Boolean>();

    /********************************************************
     * After Insert
     * *****************************************************/

    public static void handleAfterInsert(List<Contact> insertedContacts, Map<Id,Contact> newContactMap, Map<Id,Contact> oldContactMap) {
        //Create new WSH records

        Set<ID> ids = New Set<ID>();
        List<Contact> stageNotSet = New List<Contact>();

        for(Contact con : insertedContacts){
            if(con.Waterfall_Stage__c!='Open-Cold'){
                ids.add(con.Id);
            }
            else if(con.Date_Last_Contact_Actv__c != null){
                //If the contact has done something, evaluate if it meets lead score threshold
                if(con.Lead_Score__c == 'Hot' || con.Lead_Score__c == 'Warm'||con.Actual_Score__c=='A1'||
                  con.Actual_Score__c=='A2'||con.Actual_Score__c=='B1'||con.Actual_Score__c=='A3'||
                   con.Actual_Score__c=='B2'||con.Actual_Score__c=='B3'||con.Actual_Score__c=='C1'||
                   con.Actual_Score__c=='C2'||con.Actual_Score__c=='D1'){
                    //If yes, mark as AQL
                    //con.Waterfall_Stage__c = 'AQL-Sales';
                    alreadyProcessed.put(con.Id,false);
                    stageNotSet.add(new Contact(
                        ID = con.Id,
                        Waterfall_Stage__c = 'AQL-Sales',
                    ));
                }
                else{
                //Otherwise mark as inquiry
                //con.Waterfall_Stage__c = 'Open-Inquiry';
                alreadyProcessed.put(con.Id,false);
                stageNotSet.add(new Contact(
                    ID = con.Id,
                    Waterfall_Stage__c = 'Open-Inquiry',
                ));
                }
            }
            else{
                ids.add(con.Id);
            }
        }
        if(ids.size()>0){
            List<Contact> newContacts = [Select Id,AccountId,Account.ownerid,Account.Stage__c
                               From Contact Where Id in :ids];
            createBrandNewHistoryRecords(newContacts,newContactMap,oldContactMap);
        }
        if(stageNotSet.size()>0){
            update stageNotSet;
        }

    }//End of method

    /********************************************************
     * Before Update
     * *****************************************************/

    public static void handleBeforeUpdate(List<Contact> oldContacts, List<Contact> newContacts, Map<ID,Contact> newContactMap, Map<ID,Contact> oldContactMap) {
        Set<ID> ids = new Set<ID>();
        Set<ID> newIds = new Set<ID>();
        //Create empty list of contacts (we will populate this with contacts whose Waterfall Stage has changed)
        //List<Contact> updatedParents = New List<Contact>();
        for(Contact con : newContacts){

            //if stage has changed, add it to the list defined above
            String oldStage = oldContactMap.get(con.Id).Waterfall_Stage__c;
            String newStage = newContactMap.get(con.Id).Waterfall_Stage__c;
            if(oldStage != newStage && newStage != null){
                if(!alreadyProcessed.containsKey(con.Id)){
                    ids.add(con.Id);
                    alreadyProcessed.put(con.Id,true);
                }
                else if(alreadyProcessed.get(con.Id) == false){
                    newIds.add(con.Id);
                    alreadyProcessed.put(con.Id,true);
                }
                else{}//if the stage has changed but it is in the Map with true, we do nothing because it has already been processed
            }
        }
        List<Contact> updatedParents = [Select Id,AccountId,Account.ownerid,Account.Stage__c
                               From Contact Where Id in :ids];
        List<Contact> newParents = [Select Id,AccountId,Account.ownerid,Account.Stage__c
                               From Contact Where Id in :newIds];

        if(!updatedParents.isEmpty()){
            //if records have had their Waterfall Stage change, we pass them into private methods below
            //to process the updating and creation of new child Waterfall Stage History records

            updateHistoryRecords(updatedParents, newContactMap, oldContactMap);
            //This method updates existing child records associated with the contacts whose Waterfall Stage
            //has just changed with new values for next stage, call/conversation/email counts, etc.
            //and closes them out

            createNewHistoryRecords(updatedParents, newContactMap, oldContactMap);
            //This method creates new child records associated with the contacts whose Waterfall Stage
            //has just changed. 
            //It writes values from the contact/account objects to the child object.

        }

        if(!newParents.isEmpty()){
            createBrandNewHistoryRecords(newParents, newContactMap, oldContactMap);
        }

    }//End of method

     /********************************************************
     * 
     * Private Methods
     * 
     * *****************************************************/

    private static void updateHistoryRecords(List<Contact> newContacts, Map<ID,Contact> newContactMap, Map<ID,Contact> oldContactMap){
        //Quickly generate a set of Contact IDs using the map constructor
        Set<ID> ids = new Map<Id,Contact>(newContacts).keyset();
        //Query database for related account fields and child (Waterfall Stage History) records
        //associated with the contacts and generate a list
        List<Contact> updatedParents = [Select Id,(Select ID,Contact__c,Next_Stage__c,Processed_By__c,Waterfall_Stage_Related_Status__c,Date_Closed__c 
                                 from Waterfall_Stage_History__r Where Date_Closed__c = Null)
                               From Contact Where Id in :ids];
        //Create empty list to populate with child records
        List<Waterfall_Stage_History__c> childrenToUpdate = new List<Waterfall_Stage_History__c>();
        //loop through contacts and set new values to close out child records
        for(Contact con : updatedParents){
            for(Waterfall_Stage_History__c kid : con.Waterfall_Stage_History__r){
                kid.Processed_By__c = newContactMap.get(con.Id).LastModifiedById;
                kid.Date_Closed__c = DateTime.now();
                kid.Next_Stage__c = newContactMap.get(con.Id).Waterfall_Stage__c;

                //add child record to list to update
                childrenToUpdate.add(kid);
            }
        }
        if(!childrenToUpdate.isEmpty()){
            //if the list has records in it, execute DML to update child records
            update childrenToUpdate;
        }
    }//End of method

    private static void createNewHistoryRecords(List<Contact> newContacts, Map<ID,Contact> newContactMap, Map<Id,Contact> oldContactMap) {
        //Create empty list of child records to insert
        List<Waterfall_Stage_History__c> childrenToInsert = new List<Waterfall_Stage_History__c>();
        //Loop through contacts and create a new child record for each record
        for(Contact con : newContacts){
            //add to list defined above "childrenToInsert"
            childrentoInsert.add(new Waterfall_Stage_History__c(
                //Set child record values here, for opportunities, also write over Most Opportunity ID if available
                Contact__c = newContactMap.get(con.id).Id,
                Current_Stage__c = newContactMap.get(con.id).Waterfall_Stage__c,
                Previous_Stage__c = oldContactMap.get(con.id).Waterfall_Stage__c

            ));
            }
        }
        //execute DML if list is not empty (which it should never be because 
        //we only call this method if we have records whose stage has changed)
        if(!childrenToInsert.isEmpty()){
            insert childrenToInsert;
        }
    }//End of method

    private static void createBrandNewHistoryRecords(List<Contact> newContacts, Map<ID,Contact> newContactMap, Map<Id,Contact> oldContactMap) {
        //Create empty list of child records to insert
        List<Waterfall_Stage_History__c> childrenToInsert = new List<Waterfall_Stage_History__c>();
        //Loop through contacts and create a new child record for each record
        for(Contact con : newContacts){

            childrentoInsert.add(new Waterfall_Stage_History__c(
                    //Set child record values here
                    Contact__c = newContactMap.get(con.id).Id,
                    Current_Stage__c = newContactMap.get(con.id).Waterfall_Stage__c
                    //Previous_Stage__c = oldContactMap.get(con.id).Waterfall_Stage__c 

                    //We don't write old stage in this method because it is either null or an incorrect value
                    //that we then changed in our after insert trigger

                ));

            alreadyProcessed.put(con.Id,true);

        }
        //execute DML if list is not empty (which it should never be because 
        //we only call this method if we have records whose stage has changed)
        if(!childrenToInsert.isEmpty()){
            insert childrenToInsert;
        }
    }//End of method

}//End of class

Test Class:

@isTest
private without sharing class TestDemandWaterfall {

    static testMethod void testInsertContacts(){
        //insert a bunch
        Account testAcc = new Account(
            Name = 'Test'
        );
        insert testAcc;

        List<Contact> contactsToInsert = new List<Contact>();

        for(integer i=0;i<200;i++){
            Contact testContact = new Contact(
                LastName = 'Test'+i,
                AccountId = testAcc.Id
            );
            contactsToInsert.add(testContact);
        }

        Test.startTest();

        insert contactsToInsert;

        List<Waterfall_Stage_History__c> newChildRecords = [Select Id, Contact__c from Waterfall_Stage_History__c Where Contact__c = :contactsToInsert];
        System.assert(newChildRecords.size()==200,newChildRecords);

        Test.stopTest();

    }

    static testMethod void testScoreGoesUp(){
        //test score going up, creating/updating child records

        Account testAcc = new Account(
            Name = 'Test'
        );

        insert testAcc;

        Contact testContact = new Contact(
            LastName = 'Test',
            AccountId = testAcc.Id
        );

        insert testContact;

        List<Contact> result = new List<Contact>([Select Id, Waterfall_Stage__c from Contact Where Id = :testContact.Id]);
        List<Waterfall_Stage_History__c> childRecord = new List<Waterfall_Stage_History__c>([Select Id,Contact__c,Current_Stage__c,Previous_Stage__c,Next_Stage__c from Waterfall_Stage_History__c Where Contact__c = :testContact.Id]);        
        System.assert(result[0].Waterfall_Stage__c == 'Open-Cold',result[0].Waterfall_Stage__c);
        System.assert(childRecord.size() == 1, childRecord);

        Contact highScore = new Contact(
            Id = testContact.Id,
            Waterfall_Stage__c = 'AQL-Sales'
        );

        update highScore;

        //Test.startTest();

        List<Contact> newResult = new List<Contact>([Select Id, Waterfall_Stage__c from Contact Where Id = :testContact.Id]);
        List<Waterfall_Stage_History__c> childRecords = new List<Waterfall_Stage_History__c>([Select Id,Contact__c,Current_Stage__c,Previous_Stage__c,Next_Stage__c from Waterfall_Stage_History__c Where Contact__c = :testContact.Id]);
        System.assert(newResult[0].Waterfall_Stage__c == 'AQL-Sales',newResult[0]);
        System.assert(childRecords.size()==2, childRecords);

        //Test.stopTest();
    }

}

I receive a fatal error on the last System.assert and it returns a non-updated child record.

Time Started 1/28/2015 5:06 PM Class TestDemandWaterfall Method Name testScoreGoesUp Pass/Fail Fail Error Message System.AssertException: Assertion Failed: (Waterfall_Stage_History__c:{Current_Stage__c=Open-Cold, Id=a18L00000019ldTIAQ, Contact__c=003L000000XGWpaIAH}) Stack Trace Class.TestDemandWaterfall.testScoreGoesUp: line 149, column 1

I would have expected there to be two records, and the one that is returned would have had a Next_Stage__c value of AQL-Sales.

Are test classes somehow executed as only one transaction that my recursion prevention variable is preventing the handleBeforeUpdate method from firing, or am I missing something completely here?

2
  • 1
    In general, when setting up test records in a testmethod, you should reset the recursion variable to false before executing your test - i.e. the update highScore; You want your recursion variable in the test method to simulate the user action - as they can't insert and update a Contact in the same operation, the recursion variable needs to be reset. Use @TestVisible annotation
    – cropredy
    Commented Jan 30, 2015 at 0:58
  • @crop1645 I was able to get this working by adding the @TestVisible to my recursion Map and adding the following line just before the update highScore;: ContactTriggerHandler.alreadyProcessed.clear(); Am I correct in my assumption then that a testMethod acts as one continuous transaction even if in the user context it would be impossible to insert and update a record in one operation? Commented Jan 30, 2015 at 16:50

2 Answers 2

2

The reason for your issue has to do with the context of the execution of a single testmethod

  1. When your test does an insert of test Contact(s) - your ContactTrigger trigger executes and sets the static variable used to prevent recursion
  2. When your testmethod then goes on to update a specific Contact in order to exercise your trigger logic prior to verification via asserts (e.g. update highScore;), the recursion variable is still set and nothing happens

Since you added your recursion variable to prevent repeated execution of the trigger when a user or API client does something to a Contact, you need to reset the recursion variable in your testmethod before you do the DML that is under test.

There are a variety of ways to do this:

  1. Do your recursion varaible setting in a separate class with methods to set variable to true/false as well as some get method
  2. Use @TestVisible on the recursion variable in your handler class so the testmethod can directly reset to false

Note that Test.startTest() does not reset static variables, only governor limits

0

you have made your recursion variable as private , is that working in first place? try making it public and see if it works.

You must log in to answer this question.

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