1

I'm having some problems with a test class and I honestly can't figure out why. Maybe I've been staring at it for too long, but in any case - would appreciate if someone can tell me where I've gone wrong / why these particular lines aren't being run.

Background: I've created a without sharing method that should delete events if any user changes the Status__c field to 'Cancelled'. Under certain circumstances, I just want to delete it plain and simple - but in some conditions I am creating a copy of the deleted Event in a custom "Cancellation History", and link it to the appropriate Lead or Account.

In this particular case, I'm only focusing on a test method to run code which deletes records that have NULL for WhatId and WhoId (linked to nothing) - seems pretty simple.

My Test Method:

@isTest static void unlinkedEvent() {
    // Implement test code
    List<Event> unlinkedEvents = TestDataFactory.createEvents(null,205,true);
    List<Event> eventsToUpdate = new List<Event>();

    Test.startTest();

        if(!unlinkedEvents.isEmpty()){
            for(Event ev : unlinkedEvents){
                ev.Status__c = 'Cancelled';
                ev.Cancellation_Type__c = 'Millennium S.I. Initiated';
                ev.Cancellation_Reason__c = 'Booking Error';
                eventsToUpdate.add(ev);
            }
        }

        if(!eventsToUpdate.isEmpty()){
            update eventsToUpdate;
        }
    Test.stopTest();
}

TestDataFactory.createEvents:

public static List<Event> createEvents(Id record,Integer numEvents,Boolean shouldInsert) {

String account_prefix = Schema.SObjectType.Account.getKeyPrefix();
String lead_prefix = Schema.SObjectType.Lead.getKeyPrefix();

List<Event> events = new List<Event>();
    for(Integer i=0;i<numEvents;i++) {
        Event e = new Event(Subject='TestEvent' + i,
                            Cancellation_Type__c = '',
                            Cancellation_Reason__c = '',
                            Status__c = 'Open',
                            Confirmed_By__c = 'Test User',
                            Confirmed_On__c = System.now(),
                            Department__c = 'Education',
                            Activity__c = 'Training 1',
                            EndDateTime = System.now(),
                            StartDateTime = System.now(),
                            Description = 'Test Description',
                            Event_Length_In_Minutes__c = '60',
                            Event_Notes__c = 'Test Notes');
        System.debug('Event e :: ' + e + ' Subject: ' + e.Subject + ' Activity: ' + e.Activity__c);

        // If record Id is null, use a more generic Event Type (Like an Internal Meeting)
        if(record == null){
            e.Department__c = 'General';
            e.Activity__c = 'Meeting';
        }

        // If record Id is a Lead, Use Lead Centric Event Types and link WhoId to record
        else if(record != null && ((String)record).startsWith(lead_prefix)){
            e.WhoId = record;
            e.Department__c = 'Sales';
            e.Activity__c = 'Demo';
        }

        // If record Id is an Account, leave event types alone and link record to WhatId
        else if(record != null && ((String)record).startsWith(account_prefix)){
            e.WhatId = record;
        }

        System.debug('WhatId: ' + e.WhatId + ' WhoId: ' + e.WhoId);

        events.add(e);
    }
    if(shouldInsert) insert events;
    System.debug('Events: ' + events);
    return events;
}

My expectation is that this test method should execute the eventsToDelete.add(e); // THIS SHOULD RUN BECAUSE CONDITIONS MET??? line below, based on the fact that I am passing null as the record Id to TestDataFactory.createEvents, which should result in 205 events being created with no assigned WhatId or WhoId:

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>();
    String account_prefix = Schema.SObjectType.Account.getKeyPrefix();
    String lead_prefix = Schema.SObjectType.Lead.getKeyPrefix();
    Decimal eventLength = null;

    for(event e: eventMap.values()){
        if(e.Status__c == 'Cancelled' && oldEventMap.get(e.Id).Status__c != 'Cancelled'){
            // If the Event is not Linked to Anything
            if(e.WhatId == null && e.WhoId == null){
                eventsToDelete.add(e); // // THIS SHOULD RUN BECAUSE CONDITIONS MET???
                // Do not build Cancellation History record since it does not apply to any WhatId or WhoId
            }

** Rest of Code Omitted for post length purposes **

So, I have a test method that runs successfully (I know it has no asserts and therefore isn't really a test YET, I plan on adding them after I get line coverage requirement), that method runs a TestDataFactory.createEvents method and passes a null record Id which should create 205 events without any assigned WhatId or WhoId, and then the test method loops through the list of 205 events to set each one to Cancelled and add it to an update list before finally updating the list after the loop.

This method makes it past the if(e.Status__c == 'Cancelled' && oldEventMap.get(e.Id).Status__c != 'Cancelled'){ line so I presume that I am successful with inserting events and updating them to Cancelled, but the Dev Console still indicates that the commented line (eventsToDelete.add(e);) wasn't run. This of course should mean that the events WERE linked to a WhatId or WhoId and didn't pass the if(e.WhatId == null && e.WhoId == null){ check, but they shouldn't be because the TestDataFactory method shouldn't assign either of those fields if record == null - What am I missing?

  • Add a debug just before the e.whatid == null part to debug the lead record. Is the what and who ID really null? If not, is there any other code setting the value? – Eric Sep 14 '16 at 21:18
  • More debugging has yielded that there IS an Id being set for WhatId, but the only Unit Test being run is this one and there are no workflows or other apex methods that are being triggered by this insert that should be arbitrarily assigning a WhatId to all newly inserted null Events. I AM using a @testSetup method in my Test Class, trying to conform to new practices (build your data once, test it multiple times). And I am calling the same DataFactory method 3 times to 3 separate lists in setup... but this is not the outcome I expected from doing it this way. I'll dig farther and report back. – Morgan Marchese Sep 15 '16 at 1:47
  • Well I've pretty much isolated it to the @testSetup portion of my test Class, and it changes the scope of my question - not sure if I should edit this question and change it dramatically, or create a new question – Morgan Marchese Sep 15 '16 at 13:34
  • create a new question – Eric Sep 15 '16 at 13:35
  • I figured it out, sort of. Answered below – Morgan Marchese Sep 15 '16 at 18:31
0

After much debugging, I realized that for some reason large chunks of places where I expected the method to be run weren't being run at all. By adding a debug line at the very beginning of my loop, I saw entire passes that should have gone through the loop were being skipped. This ultimately resulted in the null Events in the loop already having a new Status__c value of Cancelled AND an old value of Cancelled in the oldEventMap by the time they were actually run through the EventCancelHeader.cancelledEvent(); method that I was trying to provide coverage for.

Since my first if() condition is expecting to see a current Status__c of Cancelled and an oldEventMap Status__c of Open, (which was no longer the case since we missed that bit), it terminated on the first line in the loop. I hadn't even had a chance yet to confirm that both WhoId and WhatId were null and add them to the list because I'd failed out on my first if() condition.

When I realized that, I went back to the source of my data: the Trigger

The trigger was wrapped in a Trigger.isAfter && Trigger.isUpdate block, but to ensure that the trigger only ran once per modified record it was also wrapped in a "RunOnce" style Boolean to disable recursion.

Trigger Example:

      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;
      }

In a real world environment, this wouldn't be a problem, as it would run all of the updates only once as expected - but in the case of test classes sometimes the same philosophy isn't true.

By adding a ValidatorClass.CancelledEventTriggerFired = false before my DML statements in all of the associated testMethods (and in the DataFactory), I'm able to reset this Boolean to false before each DML to have my changes passed through the method.

  • That code prevents recursion. In tests you sometimes have to do this ValidatorClass.CancelledEventTriggerFired = false just prior to a DML so the code runs. Do not comment it out of the trigger. Since test classes happen in one transaction you have to find ways to work around doing what would be multiple transaction in real life – Eric Sep 15 '16 at 18:43
  • AH Ha! I knew its purpose is to stop recursion (I implemented it in this case), but didn't know that test classes happen in a single transaction between the initial insert and eventual updates. I was actually just writing a new question to ask if test classes happen in a single transaction in this case. - So if I run 40 different unit tests at the same time, they're all 1 transaction (even though Test.StartTest gives me a separate set of limits within that transaction)? – Morgan Marchese Sep 15 '16 at 18:49
  • I'll also edit my answer to more appropriately suggest the use of ValidatorClass.CancelledEventTriggerFired = false rather than commenting out important recursion protection – Morgan Marchese Sep 15 '16 at 18:50
  • NO. A single method is in one transaction. So if your trigger fired in that method, and then you do something else in the same method where you need that trigger code to run again you need to reset the recursion flag. Each method runs in its own transaction. – Eric Sep 15 '16 at 18:53
  • That makes more sense. However, I was only running one test method in this case. The test class is utilizing the new-ish testSetup feature from Spring '15 (developer.salesforce.com/releases/release/Spring15/TestClasses), so the actual testMethod wasn't building any data, just querying and updating data that was inserted during the @testSetup - perhaps that is the reason that I had an issue when running a single test method? (does testSetup build the data which fires the trigger before it runs the testMethod in same transaction)? Would need to test it but that seems like odd behavior – Morgan Marchese Sep 15 '16 at 19:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.