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
devrequirementdevrequirement 

12% Code Coverage for a Batch Class

Below is the Test Class for the Batch class in the following snippet of the code. The test coverage stops right here : global Database.QueryLocator start(Database.BatchableContext BC){

In need of guidance and help.

 

TEST CLASS

@isTest(SeeAllData =true)

    private class UpdateEmailOptOutFieldBatchableTest{
    
    static testmethod void testUpdateEmailOptOutFieldBatchable() {
        
       
    
        Contact ConObj1 = new Contact();
        ConObj1.FirstName = 'Paul';
        ConObj1.LastName = 'McCartney';
        ConObj1.Email='PM@p.com';
        //ConObj1.Id ='0038000001HeyJuDEE';
        ConObj1.HasOptedOutOfEmail=False;
        //ConObj1.LastModifiedDate = 
        insert conObj1;
        
        Contact ConObj2 = new Contact();
        ConObj2.FirstName = 'John';
        ConObj2.LastName = 'Lennon';
        ConObj2.Email='PM@p.com';
        //ConObj2.Id ='0038000002ImaGiNEE';
        ConObj2.HasOptedOutOfEmail=False;
        //ConObj2.LastModifiedDate = 
        insert conObj2;
            
        Contact ConObj3 = new Contact();
        ConObj3.FirstName = 'Ringo';
        ConObj3.LastName = 'Star';
        ConObj3.Email='RS@p.com';
        //ConObj3.Id ='0038000003LsdSkYEE';
        ConObj3.HasOptedOutOfEmail=False;
        //ConObj3.LastModifiedDate = 
        insert conObj3;
        
        Contact ConObj4 = new Contact();
        ConObj4.FirstName = 'George';
        ConObj4.LastName = 'Harrison';
        ConObj4.Email='RS@p.com';
        //ConObj4.Id ='0038000004GenTlYEE';
        ConObj4.HasOptedOutOfEmail=True;
        //ConObj4.LastModifiedDate = 
        insert conObj4;

        Contact ConObj5 = new Contact();
        ConObj5.FirstName = 'Led';
        ConObj5.LastName = 'Zeppelin';
        ConObj5.Email='LZ@p.com';
        //ConObj5.Id ='0038000004HeaVeNNN';
        ConObj5.HasOptedOutOfEmail=False;
        //ConObj5.LastModifiedDate = 
        insert conObj5;

        Contact ConObj6 = new Contact();
        ConObj6.FirstName = 'Jimi';
        ConObj6.LastName = 'Hendrix';
        ConObj6.Email='LZ@p.com';
        //ConObj6.Id ='0038000004CasTlEEE';
        ConObj6.HasOptedOutOfEmail=True;
        //ConObj6.LastModifiedDate = 
        insert conObj6;
        
        List<Contact> ContactListall = new List<Contact>();
        ContactListall.add(conObj1);
        ContactListall.add(conObj2);
        ContactListall.add(conObj3);
        ContactListall.add(conObj4);
        ContactListall.add(conObj5);
        ContactListall.add(conObj6);
 
         Test.startTest();
         //String query = 'SELECT Id, Name, Email, HasOptedOutOfEmail, LastModifiedDate from CONTACT'; 
         //UpdateEmailOptOutFieldBatchable objUpdateEmailOptOutFieldBatchable = new UpdateEmailOptOutFieldBatchable(query);
         UpdateEmailOptOutFieldBatchable objUpdateEmailOptOutFieldBatchable = new UpdateEmailOptOutFieldBatchable();
         ID batchprocessid = Database.executeBatch(objUpdateEmailOptOutFieldBatchable);
         System.abortJob(batchprocessid);
         Test.stopTest(); 
          }
}

 

BATCH CLASS

global Class UpdateEmailOptOutFieldBatchable implements Database.Batchable<sObject>{
// declaring the variable
global final String query;
 
/* Constructor method to fetch contacts from the database*/
 
global UpdateEmailOptOutFieldBatchable(){
    query = 'SELECT Id, Name, Email, HasOptedOutOfEmail, LastModifiedDate from CONTACT ORDER BY LastModifiedDate DESC';
}
 
/*Start method to be invoked*/
global Database.QueryLocator start(Database.BatchableContext BC){
    return Database.getQueryLocator(query);
}
 
global void execute(Database.BatchableContext BC, List<Contact> allContactList)
{
// declaring the variables
                List <Contact> ContactsToUpdate = new List<Contact>();
                Set <String> ContactIDsToUpdate = new Set<String>();
                for(integer i=0; i<allContactList.size()-1; i++)
                {
                                for(integer j=i+1; j<allContactList.size()-1; j++)
                                {
                                                If (allContactList[i].email == allContactList[j].email && ContactIDsToUpdate.contains(allContactList[j].Id)==False)
                                                {
                                                                ContactIDsToUpdate.add(allContactList[j].Id);
                                                                allContactList[j].HasOptedOutOfEmail=allContactList[i].HasOptedOutOfEmail;
                                                                ContactsToUpdate.add(allContactList[j]);
                                                }
                                }
                }             
                If (ContactsToUpdate.isempty()==False)
                {
                                update ContactsToUpdate;
                }
}
global void finish(Database.BatchableContext BC){
    Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
    mail.setToAddresses(new String[]{'rkshrest@gmail.com'});
    mail.setReplyTo('rkshrest@gmail.com');
    mail.setSenderDisplayName('Batch Processing');
    mail.setSubject('Batch Process Completed');
    mail.setPlainTextBody('Batch Process of Email Opt Out field update for duplicate contact records has completed');
    Messaging.sendEmail(new Messaging.SingleEmailMessage[] {mail});
    }
}
Best Answer chosen by Admin (Salesforce Developers) 
sfdcfoxsfdcfox

We should probably take a step back and observe what's going on before we go down this road much further. Assuming I'm reading your code correctly, it appears that you're trying to toggle "email opt out" for all contacts by email when the value changes. It would seem to me that your best choice would be a trigger, not a batch class, which is not selective enough anyways. Try this:

 

// CLASS
public class ContactCheck {
  public static boolean recursive;
  static {
    recursive = false;
  }
  public static void updateOthers( Contact[ ] newContacts ) {
    Map< String, Boolean > optOut = new Map< String, Boolean >( );
    Contact[ ] updateContacts = new Contact[ 0 ];
    for( Contact con:newContacts ) {
      optOut.put( con.email, con.hasoptedoutofemail );
    }
    optout.remove( null ); // ignore blank emails
    optout.remove( '' );   // ignore blank emails
    for( Contact con:[SELECT Id,Email,HasOptedOutOfEmail FROM Contact WHERE Email IN :optOut.keySet( )] ) {
      if( con.hasoptedoutofemail != optout.get( con.email ) ) {
        con.hasoptedoutofemail = optout.get( con.email );
        updatecontacts.add( con );
      }
    }
    if( !updatecontacts.isempty( ) ) {
      update updatecontacts;
    }
  }
}

// TRIGGER
trigger ContactCheckTrigger on Contact ( after insert, after update ) {
  if( !ContactCheck.recursive ) {
    ContactCheck.recursive = true;
    ContactCheck.updateOthers( Trigger.new );
  }
}

 

 

//////////////////////////

 

That said, if you want to use a batch class, you'll need to think it through again; your code won't work well because of boundary issues, incorrect ordering of query, non-optimized query, etc. Here's some helpful tidbits:

 

* Filter your query at least by "Email != null" (I do this in my trigger by removing the empty keys from the email map.

* Order your query by Email, then by LastModifiedDate. This will increase the chances that relevant contacts will be next to each other.

* Consider running a query inside your execute method to obtain data (increases accuracy).

 

Here's a possible solution:

 

global class EmailOptOutBatch implements Database.Batchable< sobject > {
    global EmailOptOutBatch( ) {
        this( false );
    }

    global EmailOptOutBatch( boolean testMode ) {
        this.testMode = testMode;
    }

    global boolean testMode;

    global database.querylocator start( Database.batchableContext bc ) {
        return    // All contacts sorted by email and date.
            database.getquerylocator( 'select id,email,hasoptedoutofemail,lastmodifieddate from contact order by email asc,lastmodifieddate asc'+( testMode? ' limit 200': '') );
    }
    
    global void execute( Database.BatchableContext bc, contact[ ] cons ) {
        Map< String, Boolean > optOuts = new Map< String, Boolean >( );
        contact[ ] upcons = new contact[ 0 ];

        // Find all emails in this batch.        
        for( contact con: cons ) {
            optOuts.put( con.email, con.hasoptedoutofemail );
        }

        // Ignore blank values
        optOuts.remove( null );
        optOuts.remove( '' );

        if( !optOuts.isEmpty( ) ) {
            // Find the most recent opt out value
            for( contact con: [select id,email,hasoptedoutofemail from contact where email in :optouts.keyset( ) order by lastmodifieddate asc] ) {
                optouts.put( con.email, con.hasoptedoutofemail );
            }
            // Find all non-conforming contacts and add them to the list
            for( contact con: cons ) {
                if( optouts.containsKey( con.email ) && con.hasoptedoutofemail != optouts.get( con.email ) ) {
                    con.hasoptedoutofemail = optouts.get( con.email );
                    upcons.add( con );
                }
            }
            if( !upcons.isempty( ) ) {
                update upcons;
            }
        }
    }
    
    global void finish( Database.BatchableContext bc ) {
        // We're done.
    }
}

Fully implemented, just use your test method on it with name changes, should be okay.

 

Note the use of maps instead of nested for loops ( for ... for ... ), a query to check for the most recent value, etc. Everything's commented, so it should be fairly easy to understand.

All Answers

sfdcfoxsfdcfox

Don't abort the job; this means it won't be executed (and remember, it runs in a "sandbox" mode, so no data will actually be modified). Asynchronous code runs when "Test.stopTest()" is called.

devrequirementdevrequirement

SF,

 

Thanks for your reply.

 

I did commented out the system.abort line.

The code coverage increased to 20%.

However, a test failure occured with following message:

 

Method Name:

UpdateEmailOptOutFieldBatchableTest.testUpdateEmailOptOutFieldBatchable

 

 

Message:

System.UnexpectedException: No more than one executeBatch can be called from within a testmethod. Please make sure the iterable returned from your start method matches the batch size, resulting in one executeBatch invocation.

 

Stack Trace:

External entry point

 

Please help!!

 

DR

 

sfdcfoxsfdcfox

Like is says, you need to make sure that you're returning no more than one batch worth of data (200 rows) when testing. Usually this means passing a parameter into the batch class so that it uses a different query depending on if you're in test mode or not. Try this:

 

global UpdateEmailOptOutFieldBatchable(){
    this(false);
}

global UpdateEmailOptOutFieldBatchable(boolean testMode) {
    query = 'SELECT Id, Name, Email, HasOptedOutOfEmail, LastModifiedDate from CONTACT ORDER BY LastModifiedDate DESC'+(testMode?' LIMIT 200':'');
}

Then, change your test code to use "new UpdateEmailOptOutFieldBatchable(true)" instead of the default constructor.

devrequirementdevrequirement

SF,

 

Thank you so much for your time and consideration.

 

So in order to test this batch class, I created a contact and it's duplicate contacts(i.e. same email id) with different values for the Email Opt Out field.

 

I executed the batch from developer console using,

Database.executeBatch(new UpdateEmailOptOutFieldBatchable());

 

All the batches processed without failure. However, the duplicate contacts were not updated :(
Can you please give a one last look to my batch apex code to see if I have missed some logic?

 

Thanks!!!!

devrequirementdevrequirement

Help Please!!

sfdcfoxsfdcfox

We should probably take a step back and observe what's going on before we go down this road much further. Assuming I'm reading your code correctly, it appears that you're trying to toggle "email opt out" for all contacts by email when the value changes. It would seem to me that your best choice would be a trigger, not a batch class, which is not selective enough anyways. Try this:

 

// CLASS
public class ContactCheck {
  public static boolean recursive;
  static {
    recursive = false;
  }
  public static void updateOthers( Contact[ ] newContacts ) {
    Map< String, Boolean > optOut = new Map< String, Boolean >( );
    Contact[ ] updateContacts = new Contact[ 0 ];
    for( Contact con:newContacts ) {
      optOut.put( con.email, con.hasoptedoutofemail );
    }
    optout.remove( null ); // ignore blank emails
    optout.remove( '' );   // ignore blank emails
    for( Contact con:[SELECT Id,Email,HasOptedOutOfEmail FROM Contact WHERE Email IN :optOut.keySet( )] ) {
      if( con.hasoptedoutofemail != optout.get( con.email ) ) {
        con.hasoptedoutofemail = optout.get( con.email );
        updatecontacts.add( con );
      }
    }
    if( !updatecontacts.isempty( ) ) {
      update updatecontacts;
    }
  }
}

// TRIGGER
trigger ContactCheckTrigger on Contact ( after insert, after update ) {
  if( !ContactCheck.recursive ) {
    ContactCheck.recursive = true;
    ContactCheck.updateOthers( Trigger.new );
  }
}

 

 

//////////////////////////

 

That said, if you want to use a batch class, you'll need to think it through again; your code won't work well because of boundary issues, incorrect ordering of query, non-optimized query, etc. Here's some helpful tidbits:

 

* Filter your query at least by "Email != null" (I do this in my trigger by removing the empty keys from the email map.

* Order your query by Email, then by LastModifiedDate. This will increase the chances that relevant contacts will be next to each other.

* Consider running a query inside your execute method to obtain data (increases accuracy).

 

Here's a possible solution:

 

global class EmailOptOutBatch implements Database.Batchable< sobject > {
    global EmailOptOutBatch( ) {
        this( false );
    }

    global EmailOptOutBatch( boolean testMode ) {
        this.testMode = testMode;
    }

    global boolean testMode;

    global database.querylocator start( Database.batchableContext bc ) {
        return    // All contacts sorted by email and date.
            database.getquerylocator( 'select id,email,hasoptedoutofemail,lastmodifieddate from contact order by email asc,lastmodifieddate asc'+( testMode? ' limit 200': '') );
    }
    
    global void execute( Database.BatchableContext bc, contact[ ] cons ) {
        Map< String, Boolean > optOuts = new Map< String, Boolean >( );
        contact[ ] upcons = new contact[ 0 ];

        // Find all emails in this batch.        
        for( contact con: cons ) {
            optOuts.put( con.email, con.hasoptedoutofemail );
        }

        // Ignore blank values
        optOuts.remove( null );
        optOuts.remove( '' );

        if( !optOuts.isEmpty( ) ) {
            // Find the most recent opt out value
            for( contact con: [select id,email,hasoptedoutofemail from contact where email in :optouts.keyset( ) order by lastmodifieddate asc] ) {
                optouts.put( con.email, con.hasoptedoutofemail );
            }
            // Find all non-conforming contacts and add them to the list
            for( contact con: cons ) {
                if( optouts.containsKey( con.email ) && con.hasoptedoutofemail != optouts.get( con.email ) ) {
                    con.hasoptedoutofemail = optouts.get( con.email );
                    upcons.add( con );
                }
            }
            if( !upcons.isempty( ) ) {
                update upcons;
            }
        }
    }
    
    global void finish( Database.BatchableContext bc ) {
        // We're done.
    }
}

Fully implemented, just use your test method on it with name changes, should be okay.

 

Note the use of maps instead of nested for loops ( for ... for ... ), a query to check for the most recent value, etc. Everything's commented, so it should be fairly easy to understand.

This was selected as the best answer
devrequirementdevrequirement

I can't thank you enough for helping me out in this magnitude!!

It's my christmas gift :) Happy Holidays!!