0

I have a problem on my hands around getting enough code coverage for the below code I am working on.

The Scenario: When a CPQ quote is being worked and it is a primary quote I have a process builder that keeps fields updated between the primary quote and the opportunity.

One of these fields is a multi select picklist called opportunity manufacturer, and what should be populating in this field is the manufacturers of each product on the quote only listed once (hence the set to hold those values).

So when this process builder process kicks off, fields are updated and the below "AggregateManufacturerInfo" class is called from the process. This class then grabs the list of Opp IDs from the process and calls a queueable class to do the work listed above for the manufactures and populating the multi select picklist.

Here is where I had to build out the code more as with CPQ it seems to make callouts to heroku when calculation or a document is generated, and I was getting the "Too Many queueable jobs in the queue" error. So I added in the try catch block on the following code "AggregateManQueueableJob" class to check if Limits.getQueueableJobs() is greater than 0 to catch this and if the limit is greater than 0, then call a scheduled class to get around the limit of 2 per queue error.

Here is the problem my friends, this is my first scheduled class, and the test class below saves and covers all of my code except for the calling of the scheduled class in the try / catch block, and my question is what can I change or how can I get that portion covered in my test class?

How can I fake.mock more than one queueable job in the queue to get my test class to test both scenarios? My current coverage is 100% on all classes except for "AggregateManQueuableJob" which is currently at 71%. Any help you can give would be greatly appreciated :)

Initial class called from process :AggregateManufcaturerInfo"

public class AggregateManufacturerInfo {

    @InvocableMethod
    public static void setOppMan(LIST<ID> OppIds){
        System.enqueueJob(new AggregateManQueueableJob(oppIds));
    }
}

Queueable class called from above class:

public class AggregateManQueueableJob implements Queueable {
    
    public List<Id> oppList;

    public AggregateManQueueableJob (List<Id> oppIds){
        oppList = oppIds;
    }

    public void execute(QueueableContext qc){
        System.debug(oppList);
        
        Id OppId = oppList[0];
        
        Opportunity opp = [SELECT ID, Opportunity_Manufacturer_s__c FROM Opportunity
                          WHERE ID=: OppId];
        opp.Opportunity_Manufacturer_s__c = null;
        List<SBQQ__QuoteLine__c> lines = new List<SBQQ__QuoteLine__c>();
        lines = [SELECT ID, SBQQ__Product__r.Reported_Manufacturer__c, SBQQ__Quote__r.SBQQ__Opportunity2__r.Id
                FROM SBQQ__QuoteLIne__c WHERE SBQQ__Quote__r.SBQQ__Opportunity2__r.ID =: OppId AND 
                SBQQ__Quote__r.SBQQ__Primary__c = True];
        
        if(lines.size()>0){
            Set<String> mans = new Set<String>();
            for(SBQQ__QuoteLine__c linesToProcess : lines){
                if(!String.isBlank(linesToProcess.SBQQ__Product__r.Reported_Manufacturer__c)){
                    if(linesToProcess.SBQQ__Product__r.Reported_Manufacturer__c != 'Critical Start' && linesToProcess.SBQQ__Product__r.Reported_Manufacturer__c != 'INSTALL'){
                   mans.add(linesToProcess.SBQQ__Product__r.Reported_Manufacturer__c);
                    }
                }
            }
            
            if(!mans.isEmpty()){
                for(String man : mans){
                    if(!String.isBlank(opp.Opportunity_Manufacturer_s__c)){
                        opp.Opportunity_Manufacturer_s__c = opp.Opportunity_Manufacturer_s__c + ';' + man;
                    }
                    else{
                        opp.Opportunity_Manufacturer_s__c = man;
                    }
                }
            }
        }
        
        try{
            If(Limits.getQueueableJobs() > 0){
                String hour = String.valueOf(Datetime.now().hour());
                String min = String.valueOf(Datetime.now().minute());
                String ss = String.valueOf(Datetime.now().second() + 5);
                String nextFireTime = ss + '' + min + '' + hour + '**?';
                
                errorAggregateSched schProc = new errorAggregateSched();
                schProc.ProcessIds = oppList;
                System.schedule('errorAggregateSched', nextFireTime, schProc);
            }else{
            update opp;
            }
            
        }catch(Exception e){
            system.debug('Exception:::'+e.getMessage());
        }
        
    }

}

And the scheduled class that would be called if the limits error occurs:

public class errorAggregateSched  implements Schedulable {
    
    public List<ID> ProcessIds;
    
    public void execute(SchedulableContext sc) {
        System.enqueueJob(new AggregateManQueueableJob(ProcessIds));
        System.abortJob(sc.getTriggerId());
    }

}

And finally my test class:

@isTest(SeeAllData=True)
public class AggregateManufacturerInfo_Test {

     public static testMethod void tm1() {
        List<SBQQ__QuoteLine__c> quoteLines = new List<SBQQ__QuoteLine__c>();
        List<ID> oppsToProcess = new List<ID>();
        //First Test with Good Product and Reported Manufacturer
        Account a = new Account();
            a.Name = 'Test Account1';
            a.Website = 'www.test1class.com';
            a.Type = 'Prospect';
            a.Sub_type__c = 'End User';
            insert a;
        Contact c = new Contact();
            c.FirstName = 'TestClass1';
            c.LastName = 'TesterClass1';
            c.Email = 'testclass1@tester.com';
            c.Phone = '1112223398';
            c.Title = 'Test Title';
            c.AccountId = a.Id;
            insert c;
        Opportunity opp = new Opportunity();
            opp.Name = 'Test Opp 1';
            opp.Amount = 1000;
            opp.AccountId = a.Id;
            opp.Bill_To_Account__c = a.Id;
            opp.Bill_To_Contact__c = c.Id;
            Opp.CloseDate = Date.today();
            opp.StageName = '1-Qualification';
            insert opp;
        Product2 p = new Product2();
            p.Name = 'TestProduct1';
            p.ProductCode = 'TestProduct1';
            p.Product_Code_Unique__c = 'TestProduct1';
            p.Department__c = 'MDR';
            p.Type__c = 'Service';
            p.Sales_Type__c = 'For Sale';
            p.IsActive = true;
            p.SBQQ__DefaultQuantity__c = 1;
            p.Manufacturer__c = 'Critical Start';
            p.Reported_Manufacturer__c = 'DEVO';
            p.SBQQ__PricingMethod__c = 'List';
            insert p;
        SBQQ__Quote__c qt = new SBQQ__Quote__c();
            qt.SBQQ__Type__c = 'Quote';
            qt.SBQQ__Status__c = 'Draft';
            qt.SBQQ__Opportunity2__c = opp.Id;
            qt.SBQQ__Primary__c = true;
            qt.SBQQ__Account__c = a.Id;
            qt.SBQQ__PrimaryContact__c = c.Id;
            insert qt;
        SBQQ__QuoteLine__c ql = new SBQQ__QuoteLine__c();
            ql.SBQQ__Product__c = p.Id;
            ql.Department__c = 'MDR';
            ql.Category__c = 'MDR-Bundle';
            ql.SBQQ__Quantity__c = 1;
            ql.SBQQ__Number__c = 1;
            ql.SBQQ__ListPrice__c = 100;
            ql.SBQQ__ProrateMultiplier__c = 1;
            ql.SBQQ__ProratedListPrice__c = 100;
            ql.SBQQ__SpecialPrice__c = 100;
            ql.SBQQ__ProratedPrice__c = 100;
            ql.SBQQ__NetPrice__c = 100;
            ql.SBQQ__DefaultSubscriptionTerm__c = 12;
            ql.SBQQ__SubscriptionPricing__c = 'Fixed Price';
            ql.SBQQ__ProductSubscriptionType__c = 'Renewable';
            ql.SBQQ__SubscriptionType__c = 'Renewable';
            ql.SBQQ__Quote__c = qt.Id;
           insert ql;
        oppsToProcess.add(opp.Id);
        
       Test.startTest();
            AggregateManufacturerInfo.setOppMan(OppsToProcess);
         
                String CRON_EXP = '0 0 0 15 3 ? 2022';
                String hour = String.valueOf(Datetime.now().hour());
                String min = String.valueOf(Datetime.now().minute() + 1);
                String ss = String.valueOf(Datetime.now().second() + 5);
                String nextFireTime = ss + '' + min + '' + hour + '**?';
                
                errorAggregateSched schProc = new errorAggregateSched();
                schProc.ProcessIds = oppsToProcess;
                System.schedule('errorAggregateSched', CRON_EXP, schProc);
        Test.stopTest();
    }
    
}
3
  • this problem is best addressed with dependency injection so the testmethod can "inject" into the code-under-test behavior that does not exceed the reduced limits on queueables in testmethods (by skipping the enqueueJob)
    – cropredy
    Jun 15, 2021 at 19:21
  • Awesome suggestion, however how would that be coded? I am a pretty beginner developer here so have not heard the dependency injection term before, I am sorry my friend. Can you please elaborate a little? @cropredy
    – Shawn
    Jun 15, 2021 at 19:54
  • It is possible I did not explain the problem properly, What I am trying to get help on is how do we "Mock" of fake Limits.getQueueableJobs from the test class to make it look like has a limit of 1 so the first portion of my IF statement in the above class "AgrregateManQueueableJob" code so that portion of the if statement is covered for code coverage? When I run my tests the only line in the if statement being covered is the else statement. How can I make my test class ensure that both the if and the else is covered in the try / catch block?
    – Shawn
    Jun 15, 2021 at 20:02

1 Answer 1

1

You are trying to test too many things in one unit test. Instead, break it into smaller tests

Test 1 - Does the invocable start the queueable?

AggregateManufacturerInfo.setOppMan(someListofIds);
// assert that a AsyncApexJob (use SOQL) was created 

tests 2/3

for tests 2 and 3, introduce a new class LimitsUtil with a property:

public static Boolean   isEnqueueable {
    get {
        return isEnqueueable == null ? Limits.getLimitQueueableJobs() - Limits.getQueueableJobs() > 0 : isEnqueueable;}
    set;
}

Change this line in your queueable:

if(Limits.getQueueableJobs() > 0){

to:

if(LimitsUtil.isEnqueueable){ 

Test 2 - Does the queueable if branch work

AggregateManQueueableJob amqj = new AggregateManQueueableJob(someListofIds);
LimitsUtil.isEqueueable = true;  // dependency injection
amqj.execute(null);
// do asserts here for true branch

Test 3 - Does the queueable else branch work

AggregateManQueueableJob amqj = new AggregateManQueueableJob(someListofIds);
LimitsUtil.isEqueueable = false;  // dependency injection
amqj.execute(null);
// do asserts here for else branch

A few notes:

  • Your catch block needs to do something other than system.debug; silently eating errors is not best practice
  • Your test methods need to do more than just coverage, they should assert what you expect to happen
  • Because test context has severe restrictions on asynchronous job execution as opposed to PROD, it is generally adviseable to break your testing process into bits such as I outlined above. There is little need to prove that SFDC will schedule a job from a CRON expression nor that if you do System.enqueueJob that the queued job starts. Instead, you can directly test the schedulable's execute() or queueable's execute() by constructing the appropriate object and providing data to work on either via DML or through sobjects passed in the constructor.
1
  • GREAT!!! That was pretty easy to understand with your example, thank you so much! I will attempt to put this into practice and see if it cures my issues. Thank you again and I will advise either with marking the question answered, or if I encounter any other error I am not able to bypass myself. You guys are the BEST!
    – Shawn
    Jun 15, 2021 at 22:06

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.