+ Start a Discussion
Jack VolkovJack Volkov 

Best Practice for testing record updates within a for loop

In this apex class the logic is to update a custom lead field with a math.random() value.
public class leadRandomNumber {
	
	/* update lead status for leads related to the tasks */
    public static void updateRandomNumber (List<Lead> leadsFromTasks) {
    	
    	system.debug ('***** entering method leadRandomNumber.updateRandomNumber class *****');
    	
    	List<Lead> leadsToUpdate = new List<Lead>();
    	
    	List<Lead> leads = new List<Lead>([select Id, RandomNumber__c from Lead where Id IN :leadsFromTasks and Status='Attempting' and createddate = this_year and RandomNumber__c = null]);
    	system.debug('updateRandomNumber leads queried:' + leads.size());
    	// for leads related to the tasks apply a random number to the leads if they do not yet have one
    	for(lead ld: leads) {
    		if(ld.RandomNumber__c == null) {
    			Double rand = math.random();
    			ld.RandomNumber__c = rand;
    		}
    		leadsToUpdate.add(ld);
		}
		
		update leadsToUpdate;
		system.debug('updateRandomNumber leadsToUpdate: ' + leads.size());
    	
    }
    
}

This unit test verifies that inserting the task causes the logic in the apex class above to run and update the leads as expected.
 
/*
- For leads whose lead status was just updated as the result of an attempting call
- check that the random number was set
 */
 
@isTest
private class leadRandomNumberTest {

    static testMethod void insertAttemptingCall() {
    	
    	system.debug('Inserting outbound preview call tasks as a new logo rep...');
    	
    	User newLogoRep = [select id from user where isactive = true and profile.name = 'Inside Sales User' limit 1];
    	
    	QueueSobject smbQueue = [select queue.id from queuesobject where queue.name = 'SMB AE Queue'];
    	
    	Lead l = new Lead(Company='Company',LastName='Test',Phone='8885551234',Status='Open',LeadSource='Marketing', ownerid=smbQueue.queue.id);
        insert l;
        
        // bulk insert a list of calls related to the lead
        Task task = new Task(WhoId=l.Id, OwnerId=newLogoRep.Id, Type = 'Call', Five9__Five9CallType__c='Outbound Preview', Subject='Call Attempting', Status='Completed', Five9__Five9SessionId__c='fb3636336363', ActivityDate=date.today(), Five9__Five9HandleTime__c = '00:01:59', Five9__Five9WrapTime__c = '00:00:29');
        
        test.startTest();
        
        system.RunAs(newLogoRep) {
        	insert task;
        }
        
        system.debug('Asserting that the leads status was updated to Attempting and it now has a RandomNumber value...');
        
        Task insertedTask = [select Id, Status from Task where Id =: task.id];
        System.assertEquals('Completed',insertedTask.Status);
        
        // check that the lead was updated as expected
        Lead insertedLead = [select Id, Status, RandomNumber__c from Lead where Id =: l.Id];
        System.assertEquals('Attempting',insertedLead.Status);
    	System.assertNotEquals(null,insertedLead.RandomNumber__c);
    	
    	system.debug('random number set to: ' + insertedLead.RandomNumber__c);
    	
    	test.stopTest();
    
    }

}
However the test coverage for the apex class is only 56% as the updates inside the for loop are not covered.

code that needs coverage

What is the best practice for test coverage for the lines that are missing?
BritishBoyinDCBritishBoyinDC
How is leadRandomNumber invoked though? From a task on Trigger?

If so, I think you have an error in your test - the Status on the test Lead = 'Open', but your query looks for Status='Attempting'? So I don't think the loop would be invoked since no records match the query...
Jack VolkovJack Volkov
It is invoked from another class where logic is ran to update the lead status depending on call result.

The assertions in leadRandomNumberTest.insertAttemptingCall line 26-27 is passing so leadRandomNumber.updateRandomNumber is indeed invoked.

What is the best way to get test coverage for leadRandomNumber.updateRandomNumber line 14-17?
mike_chalemike_chale
You don't say what is returned by this debug statement in your unit test, but I expect it to be zero.
system.debug('updateRandomNumber leads queried:' + leads.size());
The loop isn't being executed because the list is empty. If you compare the WHERE clause in your SOQL to you're test data there are two things to address:
  1. The SOQL is looking for Lead.Status = 'Attempting' but your test class sets it to "Open"
  2. Does Lead.RandomNumber__c default to a value or does the field definition allow nulls?
BritishBoyinDCBritishBoyinDC
To add to Mike's point, you get coverage by invoking the code - so you need to ensure the test records make that happen. If there are no records in the loop, the code never gets invoked. That's why we are suggesting you see if there might be a mismatch between the lead status of the test record and the query you are using - you need that query to return a lead to inoke that loop.
Ruben Ortiz 3Ruben Ortiz 3
Jack, you need to call leadRandomNumber.updateRandomNumber(someListWithNulls) within the test class- I experimented with your code and achieved 100% coverage by calling updateRandomNumber. Below is a slightly modified example:
@isTest
private class testLeadRandomNumber {

    static testMethod void insertAttemptingCall() {   
        
        List<Case> nonNullRandomNumber = new List<Case>();
        Case nullRand = new Case();
        List<Case> nullRandList = new List<Case>();
 
      //make some new obj's with null random__c
        for(Integer x = 0; x < 20; x++){
            nullRandList.add(new Case(status='Unassigned'));
        }
        insert nullRandList;
        
        leadRandomNumber.updateRandomNumber([select id, ruben__RandomNumber__c from Case
                                             where ruben__RandomNumber__c = null]);
        //Now you could assert that the following list [select id from case where ruben__RandomNumber__c = null] has a size of zero
       //or that it's null... which would imply that your updateRandomNumber() worked.
    }
Jack VolkovJack Volkov
Condensed the class down to this.
public class leadRandomNumber {
	
	/* update random number for leads related to tasks in the trigger */
    public static void updateRandomNumber (List<Lead> leadsFromTasks) {
    	
    	system.debug ('***** leadRandomNumber.updateRandomNumber leadsFromTasks size: ' + leadsFromTasks.size());
    	
    	List<Lead> leadsToUpdate = new List<Lead>();
    	
    	for(lead ld: leadsFromTasks) {
    		if(ld.RandomNumber__c == null && ld.Status == 'Attempting') {
    			ld.RandomNumber__c = math.random();
    		}
    		leadsToUpdate.add(ld);
		}
		update leadsToUpdate;
		
		system.debug('leadRandomNumber.updateRandomNumber leadsToUpdate first random number set to: ' + leadsToUpdate.get(0).RandomNumber__c);
    	
    }
    
}
No significant changes were made in the test class.
 
/*
- For leads whose lead status was just updated as the result of an attempting call
- check that the random number was set
 */
 
@isTest
private class leadRandomNumberTest {

    static testMethod void insertAttemptingCall() {
    	
    	system.debug('Inserting outbound preview call tasks as a new logo rep...');
    	
    	User newLogoRep = [select id from user where isactive = true and profile.name = 'Inside Sales User' limit 1];
    	
    	QueueSobject smbQueue = [select queue.id from queuesobject where queue.name = 'SMB AE Queue'];
    	
    	Lead l = new Lead(Company='Company',LastName='Test',Phone='8885551234',Status='Open',LeadSource='Marketing', ownerid=smbQueue.queue.id);
        insert l;
        
        Task task = new Task(WhoId=l.Id, OwnerId=newLogoRep.Id, Type = 'Call', Five9__Five9CallType__c='Outbound Preview', Subject='Call Attempting', Status='Completed', Five9__Five9SessionId__c='fb3636336363', ActivityDate=date.today(), Five9__Five9HandleTime__c = '00:01:59', Five9__Five9WrapTime__c = '00:00:29');
        
        system.RunAs(newLogoRep) {
        	insert task;
        }
        
        system.debug('Asserting that the leads status was updated to Attempting and it now has a RandomNumber value...');
        
        Task insertedTask = [select Id, Status from Task where Id =: task.id];
        System.assertEquals('Completed',insertedTask.Status);
        
        // check that the lead was updated as expected
        Lead insertedLead = [select Id, Status, RandomNumber__c from Lead where Id =: l.Id];
        System.assertEquals('Attempting',insertedLead.Status);
    	System.assertNotEquals(null,insertedLead.RandomNumber__c);
    	
    	system.debug('leadRandomNumberTest random number set to: ' + insertedLead.RandomNumber__c);
    	
    }

}

Coverage is now 86% but only because the code is condensed so their is less to cover.  

But the logic inside of the IF statement at line 12 is still not covered. How can this not be getting invoked if the RandomNumber__c value, which default is NULL, is being set to a math.random() value?  This is the only method that would set the RandomNumber__c value. 

Does the fact that leadsFromTasks.size() = 1 prove the method was invoked?
leadsFromTasks size

 
BritishBoyinDCBritishBoyinDC
I would add a debug statement to line 11 to see what the values for the ld variable actually are - seems like the if statement doesn't think one of those if statements are true?
mike_chalemike_chale
You still are checking for Lead.Status = 'Attempting' in your function but setting Lead.Status to 'Open' in your test class. Inside your loop debug out each boolean expression separately so you can see where the mismatches are.
Ruben Ortiz 3Ruben Ortiz 3
Look through your test code, it should call   leadRandomNumber.updateRandomNumber(). The only way the if statement will be called is by calling leadRandomNumber.updateRandomNumber(). It's not going to be called indirectly by simply declaring a List<Lead>... 
Jack VolkovJack Volkov
Added some additional debug statements to the class.  The results clearly show the test lead qualifies for the function and is being updated by the function.

​Screenshot of debug log:  http://screencast.com/t/tYm1ytd8PIB
User-added image
Jack VolkovJack Volkov
The order of operations here are this.

1.  A task is inserted
2.  A task trigger runs which calls a class method which updates the lead status depening on the task the result
3.  At the end of the method that updates the lead status, a separate class method is called, which is leadRandomNumber.updateRandomNumber
4.  The method in leadRandomNumber.updateRandomNumber updates the random number value if lead status = attempting (which would have been udpdated in step 2)
mike_chalemike_chale
Without seeing exactly where the debug statements are it's hard to tell what we're seeing, but the ones on lines 7 and 12 indicate Lead.RandomNumber__c is not null, which would make one of your criteria fail. Inside the loop I would add one debug statement for each boolean condition being evaluated to help identify the problem.

Please try a simplified test that doesn't rely on using triggers; since you updated updateRandomNumber to not execute SOQL, just have a test method create a Lead in memory that you know meets the criteria, pass that lead in, and then verify it changed as expected.
Jack VolkovJack Volkov
View of the new debug statements inside the loop.
public class leadRandomNumber {
	
	/* update random number for leads related to tasks in the trigger */
    public static void updateRandomNumber (List<Lead> leadsFromTasks) {
    	
    	system.debug ('***** leadRandomNumber.updateRandomNumber leadsFromTasks size: ' + leadsFromTasks.size());
    	system.debug ('***** leadRandomNumber.updateRandomNumber leadsFromTasks lead data: ' + leadsFromTasks);
    	
    	List<Lead> leadsToUpdate = new List<Lead>();
    	
    	for(lead ld: leadsFromTasks) {
    		system.debug ('***** leadRandomNumber.updateRandomNumber ld.RandomNumber: ' + ld.RandomNumber__c);
    		system.debug ('***** leadRandomNumber.updateRandomNumber ld.Status: ' + ld.Status);
    		if(ld.RandomNumber__c == null && ld.Status == 'Attempting') {
    			ld.RandomNumber__c = math.random();
    		}
    		leadsToUpdate.add(ld);
		}
		update leadsToUpdate;
		
		system.debug('leadRandomNumber.updateRandomNumber leadsToUpdate first random number set to: ' + leadsToUpdate.get(0).RandomNumber__c);
    	
    }
    
}

Debug log:  http://content.screencast.com/users/jvolkov/folders/Jing/media/801f84f5-7946-44dd-822a-af6ad5878552/00002892.png
Jack VolkovJack Volkov
Ya it looks like the random number is getting set before entering the loop, so that's the issue.