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?
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 annotationupdate 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?