function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
mgodseymgodsey 

Unit Test Failing Because of Maximum Trigger Depth Exceeding

I'm trying to write a unit test for a trigger that captures the OwnerID of an Opportunity, and then populates a "Owner Details" field (giving us access to their entire user record) and a "Sales Manager" field (which is the user's manager).

 

I'm at 73% coverage, but the part that is not covered is the after update when the Opportunity Owner has changed. I've written into my test a scenario when the Opp Owner changes, but I'm getting the following error message:

 

System.DmlException: Insert failed. First exception on row 0; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, CopyOwnerInformation: execution of AfterUpdate caused by: System.DmlException: Update failed. First exception on row 0 with id 006S0000006jI1iIAE; first error: CANNOT_INSERT_UPDATE_ACTIVATE_ENTITY, CopyOwnerInformation: maximum trigger depth exceeded Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i] Opportunity trigger event AfterUpdate for [006S0000006jI1i]: [] Trigger.CopyOwnerInformation: line 40, column 1: []

 

Here is the relevant code from my trigger. This is the part that I am trying to cover:

 

//Insert new owner details ID if it has changed
if(opp.Owner_Details__c == null || (opp.OwnerId != oldopp.OwnerID)){
List<Opportunity> OppsWithNewOwner = [SELECT ID, OwnerID, Owner_Details__c FROM Opportunity WHERE id in:trigger.new];

for (Integer i=0; i<OppsWithNewOwner.size(); i++){                                      
    OppswithNewOwner[i].Owner_Details__c = OppsWithNewOwner[i].OwnerID;}
    
    update OppsWithNewOwner;

    }

 

And here is my test class:

 

@isTest
private class Test_CopyOwnerInformation{
    static testMethod void CopyOwnerInformation(){
    
//set up test records
        
User user1 = new User();
        user1.firstname = 'Test User 1 First Name';
        user1.lastname = 'Test User 1 Last Name';
        user1.alias = 'user1';
        user1.email = 'testuser@unittest.com';
        user1.EmailEncodingKey = 'ISO-8859-1';
        user1.username = 'testuser1@unittest.com';
        user1.TimeZoneSidKey = 'America/Chicago';
        user1.LocaleSidKey = 'en_US';
        user1.ProfileId = '00e70000000sP2v';
        user1.LanguageLocaleKey = 'en_US';
        insert user1;
        
User manager2 = new User();
        manager2.firstname = 'Test Manager 2 First Name';
        manager2.lastname = 'Test Manager 2 Last Name';
        manager2.alias = 'mgr2';
        manager2.email = 'testmanager@unittest.com';
        manager2.EmailEncodingKey = 'ISO-8859-1';
        manager2.username = 'testmanager2@unittest.com';
        manager2.TimeZoneSidKey = 'America/Chicago';
        manager2.LocaleSidKey = 'en_US';
        manager2.ProfileId = '00e70000000sP2v';
        manager2.LanguageLocaleKey = 'en_US';
        insert manager2;
        
User user2 = new User();
        user2.firstname = 'Test User 2 First Name';
        user2.lastname = 'Test User 2 Last Name';
        user2.alias = 'user2';
        user2.email = 'testuser@unittest.com';
        user2.EmailEncodingKey = 'ISO-8859-1';
        user2.username = 'testuser2@unittest.com';
        user2.manager = manager2;
        user2.TimeZoneSidKey = 'America/Chicago';
        user2.LocaleSidKey = 'en_US';
        user2.ProfileId = '00e70000000sP2v';
        user2.LanguageLocaleKey = 'en_US';
        insert user2;
  
Account account = new Account();
        account.Name = 'Test Account';
        account.OwnerID = user1.id;
        account.web_site__c = 'test.com';
        account.industry = 'Entertainment';
        account.type = 'Advertiser';
        insert account;
        
Opportunity opportunity = new Opportunity();
        opportunity.AccountId = account.Id;
        opportunity.OwnerID = account.OwnerId;
        opportunity.Name = 'Test Opportunity';
        opportunity.StageName = 'Active';
        opportunity.CloseDate = System.today();
        opportunity.Amount = 1000.00;
        opportunity.Type = 'New Client';
        
//test  
    test.startTest();     
        insert opportunity;
    
//Validate that the Owner Details field was updated.
    opportunity = [SELECT OwnerId, Owner_Details__c, Owner_Details__r.ManagerID, SalesManager__c FROM Opportunity WHERE id = :opportunity.id];
    System.assertEquals(opportunity.OwnerId, opportunity.Owner_Details__c);
    System.assertEquals(opportunity.SalesManager__c, Opportunity.Owner_Details__r.ManagerID);

//Test that the Owner Details changes with an Opp Owner Change
   
        opportunity.OwnerId = user2.ID;
        update opportunity;
     test.stopTest();

//Validate that the Owner Details and Manager fields were updated.
    opportunity = [SELECT OwnerID, Owner_Details__c, Owner_Details__r.ManagerID, SalesManager__c FROM Opportunity WHERE id = :opportunity.id];
    System.assertEquals(opportunity.Owner_Details__c, user2.Id);
    System.assertEquals(opportunity.SalesManager__c, user2.Manager.Id);
    }
}

 Thank you for the help!

 

Best Answer chosen by Admin (Salesforce Developers) 
Andrew WilkinsonAndrew Wilkinson

In one of my previous posts I stated that this could be done with a workflow rule. I still recommend that option as a best practice. However if you wanted to pursue this for learning purposes or whatever reason you would need to query the records, populate it into a map, and call the values from the map.

 

if(Trigger.isbefore && Trigger.isUpdate){
    Set<ID> ownerDetailIds = new Set<ID>();
    Map<Id,User> ownerDetailMap = new Map<Id,User>();
    for(Opportunity o : Trigger.new)
        ownerDetailIds.add(o.Owner_Details__c);
    for(User u : [SELECT ID,ManagerID FROM User WHERE ID IN : ownerDetailIds])
        ownerDetailMap.put(u.id,u);
    for(Opportunity o : Trigger.new){
        o.Owner_Details__c = o.OwnerId;
        o.SalesManagerIDv2__c = ownerDetailMap.get(o.Owner_Details__c).ManagerID;
        o.SalesManager__c = o.SalesManagerIDv2__c;
    }

 

All Answers

Andrew WilkinsonAndrew Wilkinson

I see a few issues with your trigger. Can you post the entire code so this can be resolved?

Andrew WilkinsonAndrew Wilkinson

But a few things that need to be resolved are recursion(which could be satsfied without adding an additional update to this field. The portion displayed in this trigger should occur before update and not after update since it is the same object that is being updated. This would solve the recursion. Also, I would like to see if there are other ways to optimize this trigger. I noticed you are running DML's on individual statements instead of in bulk. This can cause issues and failures as well. Please post your code and I can resolve these for you and give you some tips.

mgodseymgodsey

Hi Andrew - thanks so much for your help! I'm new to Apex and have never done any coding before, so I really appreciate all of the tips I can get.

 

Here is the entire trigger:

 

trigger CopyOwnerInformation on Opportunity (before Insert, after Update) {

//------------Before Insert----------------------
//Copy Owner to Owner Details Field

if(Trigger.isBefore && Trigger.isInsert){
for (Opportunity x: Trigger.New) {x.Owner_Details__c=x.OwnerID;
}
}

//-------------After Update----------------------------------

//compare old and new record to see if the Sales Manager field has changed
if(Trigger.isAfter && Trigger.isUpdate){

//create map to compare old Opp values
for (Opportunity opp: trigger.new) {
    Opportunity oldopp = Trigger.oldMap.get(opp.ID);

//Insert new owner details ID if it has changed
if(opp.Owner_Details__c == null || (opp.OwnerId != oldopp.OwnerID)){
List<Opportunity> OppsWithNewOwner = [SELECT ID, OwnerID, Owner_Details__c FROM Opportunity WHERE id in:trigger.new];

for (Integer i=0; i<OppsWithNewOwner.size(); i++){                                      
    OppswithNewOwner[i].Owner_Details__c = OppsWithNewOwner[i].OwnerID;}
    
    update OppsWithNewOwner;

    }

//Update Sales Manager Field    
if(opp.SalesManager__c == null || (opp.SalesManagerID__c != oldopp.SalesManagerID__c)){
List<Opportunity> UpdateOpps = [SELECT Id, SalesManager__c, SalesManagerID__c
                                FROM Opportunity
                                WHERE Id in: trigger.new];
// for records that require an update, copy Sales Manager ID to Sales Manager field                                
    for (Integer i=0; i<UpdateOpps.size(); i++){
        UpdateOpps[i].SalesManager__c = UpdateOpps[i].SalesManagerID__c;
        }                             
    Update UpdateOpps;
}
}
} // end of after update
} // end of trigger

 

mgodseymgodsey

I'm sure there is a ton to fix in the trigger, but I should also mention that I haven't run into the max trigger depth error when making changes in the UI. It's only occuring in my test method.

 

Thanks again! 

Andrew WilkinsonAndrew Wilkinson

Made a few changes. I commented out the after code and turned it into a before code.

 

trigger CopyOwnerInformation on Opportunity (before update, before Insert, after Update) {

//------------Before Insert----------------------
//Copy Owner to Owner Details Field

if(Trigger.isBefore && Trigger.isInsert){
for (Opportunity x: Trigger.New) {x.Owner_Details__c=x.OwnerID;
}
}

//-------------Before Update---------------------------------

if(Trigger.isbefore && Trigger.isUpdate){
    for(Opportunity o : Trigger.new){
        if(o.Owner_Details__c == null || (o.OwnerId != Trigger.oldMap.get(o.Id).OwnerId)){
            o.Owner_Details__c = o.OwnerId;   
        }
        if(o.SalesManager__c == null || (o.SalesManagerID__c != Trigger.oldMap.get(o.Id).SalesManagerID__c)){
            o.SalesManager__c = o.SalesManagerID__c;   
        }
    }

}

//-------------After Update----------------------------------

//compare old and new record to see if the Sales Manager field has changed
/*if(Trigger.isAfter && Trigger.isUpdate){

//create map to compare old Opp values
for (Opportunity opp: trigger.new) {
    Opportunity oldopp = Trigger.oldMap.get(opp.ID);

//Insert new owner details ID if it has changed
if(opp.Owner_Details__c == null || (opp.OwnerId != oldopp.OwnerID)){
List<Opportunity> OppsWithNewOwner = [SELECT ID, OwnerID, Owner_Details__c FROM Opportunity WHERE id in:trigger.new];

for (Integer i=0; i<OppsWithNewOwner.size(); i++){                                      
    OppswithNewOwner[i].Owner_Details__c = OppsWithNewOwner[i].OwnerID;}
    
    update OppsWithNewOwner;

    }

//Update Sales Manager Field    
if(opp.SalesManager__c == null || (opp.SalesManagerID__c != oldopp.SalesManagerID__c)){
List<Opportunity> UpdateOpps = [SELECT Id, SalesManager__c, SalesManagerID__c
                                FROM Opportunity
                                WHERE Id in: trigger.new];
// for records that require an update, copy Sales Manager ID to Sales Manager field                                
    for (Integer i=0; i<UpdateOpps.size(); i++){
        UpdateOpps[i].SalesManager__c = UpdateOpps[i].SalesManagerID__c;
        }                             
    Update UpdateOpps;
}
}
}*/ // end of after update
} // end of trigger

 This can all be done with a before update. But I am pretty sure you don't even need the trigger and this should be done in a workflow rule.

mgodseymgodsey

Hi Andrew - thank you again for your help! I'll test this now and let you know how it goes and after mark it as a solution.

 

The reason this wouldn't work with workflow rules was that the SalesManager__c and Owner_Details__c fields are user lookup fields, and field updates with look up fields are pretty restrictive. You can just choose a specific person, not update it with an ID.

mgodseymgodsey

I'm running into a bit of trouble. The SalesManagerID__c field is a formula that uses the Owner_Details__c field to get the user's manager's ID. However, with the way the trigger is set up, the Sales Manager ID that is being grabbed to update SalesManager__c is the value from before the trigger firing. It's updating to match new Owner_Details__c field.

 

I suppose I could create a Sales Manager ID field that is not a formula and update that in the trigger as well, but is there a way that does not involve making a new field?

 

I tried the following, but now it's not updating the Sales Manager field at all.

 

if(Trigger.isbefore && Trigger.isUpdate){
    for(Opportunity o : Trigger.new){
        if (o.Owner_Details__c == null || (o.OwnerID != Trigger.oldMap.get(o.ID).OwnerID)){
            o.Owner_Details__c = o.OwnerId;
            
        if (o.SalesManager__c == null || (o.SalesManagerID__c != Trigger.oldMap.get(o.ID).SalesManagerID__c)){
            o.SalesManager__c = Trigger.newMap.get(o.ID).SalesManagerId__c;
            }
         
         }
    }
}

 

Andrew WilkinsonAndrew Wilkinson

Formula fields are tricky. So you would need to replicate that field formula in the trigger

mgodseymgodsey

This is what the original formula was: Owner_Details__r.ManagerId

 

Here is what I did to try to replicate this in the trigger:

 

trigger CopyOwnerInformation on Opportunity (before Insert, before update) {

//------------Before Insert----------------------
//Copy Owner to Owner Details Field

if(Trigger.isBefore && Trigger.isInsert){
for (Opportunity x: Trigger.New) {
    x.Owner_Details__c=x.OwnerID;
}
}

//----------------Before Update------------------------------

if(Trigger.isbefore && Trigger.isUpdate){
    for(Opportunity o : Trigger.new){
            o.Owner_Details__c = o.OwnerId;
                system.debug('********* Owner Details ID**********' + o.Owner_Details__c);
            o.SalesManagerIDv2__c = o.Owner_Details__r.ManagerID;
                system.debug('********* Owner Details ID + Sales Manager Id **********' + o.Owner_Details__c+ '    '  
+ o.SalesManagerIDv2__c); o.SalesManager__c = o.SalesManagerIDv2__c;

 

The debug log shows that Owner Details updates properly, but the SalesManagerIDv2 field is null (even though the user has a manager  on their user record). Any idea what I'm doing wrong?

 

Am I better of just making this an "After Update" trigger again? I understand the reasoning for avoiding that, but it was working (with the exception of the Unit Test)

Andrew WilkinsonAndrew Wilkinson

In one of my previous posts I stated that this could be done with a workflow rule. I still recommend that option as a best practice. However if you wanted to pursue this for learning purposes or whatever reason you would need to query the records, populate it into a map, and call the values from the map.

 

if(Trigger.isbefore && Trigger.isUpdate){
    Set<ID> ownerDetailIds = new Set<ID>();
    Map<Id,User> ownerDetailMap = new Map<Id,User>();
    for(Opportunity o : Trigger.new)
        ownerDetailIds.add(o.Owner_Details__c);
    for(User u : [SELECT ID,ManagerID FROM User WHERE ID IN : ownerDetailIds])
        ownerDetailMap.put(u.id,u);
    for(Opportunity o : Trigger.new){
        o.Owner_Details__c = o.OwnerId;
        o.SalesManagerIDv2__c = ownerDetailMap.get(o.Owner_Details__c).ManagerID;
        o.SalesManager__c = o.SalesManagerIDv2__c;
    }

 

This was selected as the best answer
mgodseymgodsey

Thank you so much for your help! I just had to switch out one thing but the map really did the trick. I appreciate you taking so much time to help me get to the solution!

 

And I agree that using workflow before triggers is defintely the way to go. I tried using workflow for this, but I don't see anyway to create a field update that updates a user lookup field based on an ID. When creating the field update it makes you choose a specific user. But if anyone has figured out a way to do this, I would love to know!