A few months ago, I asked this question: Question regarding "RunOnce" Booleans on AfterUpdate methods in a Salesforce Transaction
The answer provided indicated that I should move my RunOnce boolean to the Trigger (rather than have it in the helper class method), so that each set of 200 records passed/batched to the trigger would have a fresh reset of the Boolean.
So - I moved it from the class up to the trigger.
Since test classes are a different scenario than real life most of the time (doing initial data setup/insert DML (in @testSetup in this case), and then updating records and doing additional update DML), and since the test class' DML was encountering a RunOnce boolean, I was taught to have my test class reset my RunOnce Boolean to false before any DML in my test class, via this post I made earlier this month: Test Code for Conditional Event Deletion is not running expected lines
So - I did that too.
However, even with the changes implemented from these 2 previous posts, I am still encountering coverage issues when attempting to pass more than 200 records through this RunOnce boolean. I know for a fact that the RunOnce boolean is causing the hiccup, because commenting it out of the trigger brings test coverage back to 92% - I also know that it's specifically with more than one batch of 200 records because if I drop the number of created Events to 199 it also gives 92%.
Ultimate Problem/Question: My Class (code below) only cares about Events that were just changed to Status__c = 'Cancelled', but when I run my test class with more than 200 records I only get 21% coverage because I never even make it past the oldMap/newMap comparison (
if(e.Status__c == 'Cancelled' && oldEventMap.get(e.Id).Status__c != 'Cancelled'){
) so of course the rest of my code which is reliant on this condition is no longer being run/covered.Why are my subsequent batches of 200 records still passing over this RunOnce if() block and interior method (other than the obvious fact that the boolean is true), when I am resetting the RunOnce boolean before every DML statement? Have I gone wrong in bulkifying this test class or method and how should I approach fixing it since obviously commenting out the RunOnce boolean isn't a fix?
CODE:
Test Class (other testMethods omitted since they are nearly identical to accountEvents() ):
@isTest
private class EventCancelHandler_TEST {
@testSetup static void setup() {
List<Event> eventsToInsert = new List<Event>();
Account a1 = new Account(Name = 'MTEV Test Account');
insert a1;
Lead l1 = new Lead(Company = 'MTEV Test Lead',
LastName = 'McTester',
FirstName = 'Test');
insert l1;
Contact c1 = new Contact(FirstName = 'MTEV First',
LastName = 'MTEV Last',
AccountId = a1.Id);
insert c1;
List<Event> accountEvents = TestDataFactory.createEvents(a1.Id,202,false);
eventsToInsert.addAll(accountEvents);
List<Event> leadEvents = TestDataFactory.createEvents(l1.Id,204,false);
eventsToInsert.addAll(leadEvents);
List<Event> nullEvents = TestDataFactory.createEvents(null,206,false);
eventsToInsert.addAll(nullEvents);
List<Event> contactEvents = TestDataFactory.createEvents(c1.Id,208,false);
eventsToInsert.addAll(contactEvents);
if(!eventsToInsert.isEmpty()){
ValidatorClass.CancelledEventTriggerFired = false;
insert eventsToInsert;
}
}
@isTest static void accountEvents() {
// Implement test code
List<Event> eventsToUpdate = new List<Event>();
Account acct = [SELECT Id FROM Account WHERE Name = 'MTEV Test Account' LIMIT 1];
List<Event> accountEvents = [SELECT Subject FROM Event WHERE WhatId = :acct.Id];
Test.startTest();
if(!accountEvents.isEmpty()){
for(Event ev : accountEvents){
ev.Status__c = 'Cancelled';
ev.Cancellation_Type__c = 'Millennium S.I. Initiated';
ev.Cancellation_Reason__c = 'Booking Error';
eventsToUpdate.add(ev);
}
}
if(!eventsToUpdate.isEmpty()){
ValidatorClass.CancelledEventTriggerFired = false;
update eventsToUpdate;
}
Test.stopTest();
}
}
Trigger (other after insert/after update portions of trigger omitted as they didn't seem relevant and would take up space - can provide if necessary):
trigger EventTrigger on Event (after insert, after update)
{
if(trigger.isAfter && trigger.isUpdate)
{
if(ValidatorClass.CancelledEventTriggerFired == false){
EventCancelHandler.cancelledEvent(Trigger.newMap, Trigger.oldMap); // copies contents from cancelled events to the Cancellation History Custom Object and then deletes the cancelled event.
ValidatorClass.CancelledEventTriggerFired = true;
}
}
}
Class (full class code, System.debugs omitted for space saving):
public without sharing class EventCancelHandler
{
public static final String account_prefix = Schema.SObjectType.Account.getKeyPrefix();
public static final String lead_prefix = Schema.SObjectType.Lead.getKeyPrefix();
public static final String contact_prefix = Schema.SObjectType.Contact.getKeyPrefix();
public static void cancelledEvent(Map<Id,Event> newEventMap,Map<Id,Event> oldEventMap)
{
Map<Id, Event> eventMap = new Map<Id, Event>([select Id, Activity__c,WhatId,WhoId,Confirmation_Status__c,Status__c,Cancellation_Reason__c,Cancellation_Type__c,Confirmed_By__c,Confirmed_On__c,Department__c,StartDateTime,EndDateTime,Event_Length_in_Minutes__c,Description,Event_Notes__c,Location,Subject from event where id in: newEventMap.keySet()]);
List<Event> eventsToDelete = new List<Event>();
List<Cancellation_History__c> cancellations = new List<Cancellation_History__c>();
List<Cancellation_History__c> cancelsToInsert = new List<Cancellation_History__c>();
List<Event> eventList = new List<Event>();
for(event e: eventMap.values()){
if(e.Status__c == 'Cancelled' && oldEventMap.get(e.Id).Status__c != 'Cancelled'){
eventsToDelete.add(e); // Add to List to Delete
eventList.add(e); // Add to List of Events to Create Cancellation History for Later
}
}
if(!eventList.isEmpty()){
cancelsToInsert = createCancelHistory(eventList); // Create Cancellation History Records based on eventList
}
if(!eventsToDelete.isEmpty()){
delete eventsToDelete;
}
if(!cancelsToInsert.isEmpty()){
insert cancelsToInsert;
}
} // end of cancelledEvent()
public static List<Cancellation_History__c> createCancelHistory(List<Event> events){
Map<Id,Account> contactAccountMap = new Map<Id,Account>();
Set<Id> contactIDs = new Set<Id>();
List<Cancellation_History__c> cancelRecords = new List<Cancellation_History__c>();
List<Contact> contacts = new List<Contact>();
// Build a Set of Contact Ids from Events linked to Contacts
for(Event e : events){
if(e.WhoId != null && ((String)e.WhoId).startsWith(contact_prefix)){
contactIDs.add(e.WhoId);
}
}
// If contactIDs set is not empty, query for contacts matching IDs in Set
if(!contactIDs.isEmpty()){
contacts = [SELECT firstName,lastName,AccountId,Account.Name FROM Contact WHERE Id in :contactIDs];
}
// Loop through Contacts List to build map of Contacts to Accounts
for(Contact c : contacts){
if(c.Account != null){
if(!contactAccountMap.containsKey(c.Id)){
contactAccountMap.put(c.Id,c.Account);
}
}
}
for(Event e : events){
Decimal eventLength = null;
if(e.Event_Length_in_Minutes__c == null){
eventLength = 0;
}
else{
eventLength = Decimal.ValueOf(e.Event_Length_in_Minutes__c);
}
Cancellation_History__c cancellation = new Cancellation_History__c(Activity__c = e.Activity__c,
Cancellation_Type__c = e.Cancellation_Type__c,
Cancellation_Reason__c = e.Cancellation_Reason__c,
Cancelled_By__c = UserInfo.getName(),
Cancelled_On__c = System.Now(),
Confirmed_By__c = e.Confirmed_By__c,
Confirmed_On__c = e.Confirmed_On__c,
Department__c = e.Department__c,
End__c = e.EndDateTime,
Start__c = e.StartDateTime,
Event_Details__c = e.Description,
Event_Length_In_Minutes__c = eventLength,
Event_Notes__c = e.Event_Notes__c,
Subject__c = e.Subject
);
// Assign the Cancellation Record to Account or Lead depending on what Event was linked to
if(e.WhoId != null && ((String)e.WhoId).startsWith(lead_prefix)){
cancellation.Lead__c = e.WhoId;
}
else if(e.WhoId != null && ((String)e.WhoId).startsWith(contact_prefix)){
cancellation.Contact__c = e.WhoId;
if(contactAccountMap.containsKey(e.WhoId)){
cancellation.Account__c = contactAccountMap.get(e.WhoId).Id;
}
}
if(e.WhatId != null && ((String)e.WhatId).startsWith(account_prefix)){
cancellation.Account__c = e.WhatId;
}
cancelRecords.add(cancellation);
}
if(!cancelRecords.isEmpty()){
return cancelRecords;
}
else{
return null;
}
} // end of createCancelHistory()
} // end of class EventCancelHandler
Many thanks for your time and assistance, as always.
Set<Id> processedRecords
instead of a black/whiteBoolean
flag.