0

Hi I have a batch that works on process instance object, i am struck how to achieve test class coverage for this as ElapsedTimeInHours is a system field and when i am creating approval instance and submitting for approval in test class, obvious the elapsed time would be around 0.001 like that. Can anyone help in achieving the test coverage?

global class FreightApprovalProcessBatch implements Database.Batchable<sobject>,Database.Stateful {

List<Id> userList24 = new List<Id>();
List<Id> userList48 = new List<Id>();
List<Id> userList72 = new List<Id>();

global Database.QueryLocator start(Database.BatchableContext bc)
{        
    return 
        Database.getQueryLocator('SELECT ActorId,ElapsedTimeInHours, ProcessInstance.TargetObjectId,ProcessInstance.TargetObject.Name FROM ProcessInstanceWorkitem WHERE ElapsedTimeInHours > 24 AND ProcessInstanceId IN (SELECT Id FROM ProcessInstance WHERE Status = \'Pending\')');                         
}   

global void execute(Database.BatchableContext bc, List<ProcessInstanceWorkitem> scope)
{        
    for(ProcessInstanceWorkitem p : scope)           
    {           
        Schema.SObjectType objectType = p.ProcessInstance.TargetObjectId.getSobjectType();
        if(objectType.getDescribe().getName().equals('Custom Object'))
        {              
            if(p.ElapsedTimeInHours >= 24 && p.ElapsedTimeInHours < 48 )
            {
                userList24.add(p.ActorId);                 
            }

            else if(p.ElapsedTimeInHours >= 48 && p.ElapsedTimeInHours < 72)
            {
                userList48.add(p.ActorId);                 
            }

            else if(p.ElapsedTimeInHours >= 72)
            {
                userList72.add(p.ActorId); 
            }
        }             
    }
}
global void finish(Database.BatchableContext bc)
{

    List <EmailTemplate> listTemplate = [Select id,name from EmailTemplate where name = 'Email Template 1' OR name = 'Email Template 2' OR name = 'Email Template 3'];   
    Id templateId24;
    Id templateId48;
    Id templateId72;
    for(EmailTemplate objEmailTemplate : listTemplate){

        if(objEmailTemplate.name == 'Email Template 1')
        {
            templateId24 = objEmailTemplate.Id;                
        }

        else if(objEmailTemplate.name == 'Email Template 2')
        {
            templateId48 = objEmailTemplate.Id;
        }

        else if(objEmailTemplate.name == 'Email Template 3')
        {
            templateId72 = objEmailTemplate.Id;
        }          
    }

    Messaging.MassEmailMessage emails24=new Messaging.MassEmailMessage(); 
    emails24.setTargetObjectIds(UserList24);       
    emails24.setTemplateID(templateId24);
    emails24.setSaveAsActivity(false);
    Messaging.SendEmail(New Messaging.MassEmailMessage[]{emails24}); 

    Messaging.MassEmailMessage emails48=new Messaging.MassEmailMessage(); 
    emails48.setTargetObjectIds(UserList48);
    emails48.setTemplateID(templateId48);
    emails48.setSaveAsActivity(false);       
    Messaging.SendEmail(New Messaging.MassEmailMessage[]{emails48});

    Messaging.MassEmailMessage emails72=new Messaging.MassEmailMessage(); 
    emails72.setTargetObjectIds(UserList72);
    emails72.setTemplateID(templateId72);
    emails72.setSaveAsActivity(false);       
    Messaging.SendEmail(New Messaging.MassEmailMessage[]{emails72});   
}    

}

Test class:

         Approval.ProcessSubmitRequest req = new Approval.ProcessSubmitRequest();
    req.setComments('Submitting for approval');
    req.setNextApproverIds(new Id[] {UserInfo.getUserId()});
    req.setObjectId(opp2.id);
    req.setSubmitterId(userinfo.getuserId());
    req.setProcessDefinitionNameorid('testv1');
    req.setSkipEntryCriteria(true);

    //Submit the approval request
    Approval.ProcessResult result = Approval.process(req);

    system.debug('status for approval::'+result.getInstanceStatus());

    list<ProcessInstanceWorkitem> pItem = [Select ActorId,Actor.Name,ElapsedTimeInHours, ProcessInstance.TargetObjectId,ProcessInstance.TargetObject.Name from ProcessInstanceWorkitem where ProcessInstance.TargetObjectId =: opp2.id];

    List<ProcessInstanceWorkitem> pworkLst = [select id,ActorId,ElapsedTimeInHours,ProcessInstanceId,actor.email,ProcessInstance.TargetObjectId from ProcessInstanceWorkitem where ProcessInstance.TargetObjectId =: opp2.id];
    system.debug('pworkLst is ---------'+pworkLst.size()); 
  • Not familiar enough with testing Approval processes for a full answer...but have you looked into using test.loadData or deserializing json to set system fields? – Derek F Mar 1 at 18:40
  • @DerekF thanks for ur response. Tried deserializing method, but field is not writable exception. – Vivian Mar 1 at 18:53
0

We use a design pattern that helps in this sort of case.

Firstly, do not provide the logic directly in the batch, but rather have a separate class responsible for this processing.

Make sure this "processor class" exposes methods that receive the parameters from your batch methods as needed (e.g. the list of objects given to execute).

This separate class is instantiated and used by the batch, but is also testable directly. You should be able to set up in-memory objects with the required field values for its unit tests.

If this still isn't enough, e.g. because the elapsed time is a formula field, you can change the processor so that it processes instances of an Apex class that mimics or wraps the SObject of interest, providing properties to retrieve the "field values"; the class can provide two constructors - one that takes the real SObject instance and another that takes whatever parameters you need to make the test work. The properties can be initialized from the SObject or these parameters depending on which constructor is used.

The test would instantiate them using the latter constructor whilst the batch would create them directly from the SObjects received in execute.

This extra effort is worth it if you want to be able to properly unit test your logic. You can then have a separate test for the batch that simply verifies that the logic is invoked for each relevant SObject (basically confirming the query locator is correct).

(We actually go further and provide a primitive "dependency injection" mechanism that allows us to mock out the processor class used by the batch when testing the batch, for example.)

The use of explicit Test.isRunningTest() calls in production code is, IMHO, a terrible idea and an anti-pattern. It is a quick and dirty solution and therefore is often used by people who don't have the knowledge and experience to design code for testing.

-1

If i understood correctly, the main issue you have is, not able to reach the code inside of IF statements because p.ElapseTimeInHours value is very small in test.

I would suggest adding Test.isRunningTest() as an OR condition in those IF statements in the execute method. The code will then look like something like this.

if((p.ElapsedTimeInHours >= 24 && p.ElapsedTimeInHours < 48) || Test.isRunningTest()){
    userList24.add(p.ActorId);  
}else if((p.ElapsedTimeInHours >= 48 && p.ElapsedTimeInHours < 72) || Test.isRunningTest()){
    userList48.add(p.ActorId);
}else if(p.ElapsedTimeInHours >= 72 || Test.isRunningTest()){
    userList72.add(p.ActorId); 
}

Link for Reference

  • Please see my answer for a better way to design for testing. Having conditional code in the production code just for tests is a bad idea. You can easily completely change the behaviour of the code under test in a way that means thing appear to work well in testing but fail in production. It is also what you could call "code smell". – Phil W Mar 2 at 0:04
  • @PhilW I didn't go as deep as you are. You are right about the pattern. The code will need whole lot of refactoring to respect the best practice. Cheers. – Nikola Naumovski Mar 2 at 1:09

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.