Q&A: Are there problems using System.abortJob() in Test Methods to Avoid Depth Max Governor Limit

Been posting here a bit around the ideas of Queueable Chains and test Methods. All of you have been very helpful. I had one last piece I ran into during deployment that I had to fix. The code snippet is below with the test classes, followed by my understanding of why it works.

Problem

I have a class that has a method dowork(). The method itself runs is just a method that enqueues a method. However, it calls queues up an inner class that begins a queued chain. So I had to figure out how to write a test class for a void method that only purpose is to make it easy for other devs to call the method. Additionally, the Queueable class that is enqueued also makes callouts. Lots of Async going on here.

Class with doWork()

public with sharing class SandboxRefreshHelper {
     

    public static void dowork(){
        
        System.debug('START CHAIN');
        System.enqueueJob(new que_1_getParentCampaigns ()); 
   }
    
    public class que_1_getParentCampaigns implements Queueable, Database.AllowsCallouts{
        @TestVisible SystemEnqueueJob executeNext = new ProductionEnqueueJob();
        public void execute(QueueableContext con){
            System.debug('GETTING PARENT CAMPAIGNS'); 
            SandboxDataRefresh.getFullBoxParentCampaigns(); // gets the parent campaigns
            System.debug('QUE: ChildCampaigns');
            executeNext.enqueueJob(new que_2_getChildCampaigns());    
            
        }
    }

    public class que_2_getChildCampaigns implements Queueable, Database.AllowsCallouts{
        @TestVisible SystemEnqueueJob executeNext = new ProductionEnqueueJob();
        public void execute(QueueableContext con){ // gets the parent campaigns
            System.debug('GETTING CHILD CAMPAIGNS'); 
            SandboxDataRefresh.getFullBoxChildCampaigns(); 
            System.debug('QUE: TraitSchools');
            executeNext.enqueueJob(new que_3_getTraitSchools());       
        }
    }  

Test Method for Class

@isTest
    static void testQueChain(){
            StaticResource r = [SELECT Id , Body FROM StaticResource WHERE Name = 'TestResponse'];
            CalloutService_Mock mock = new CalloutService_Mock();     
            String query = HttpRequester.getQuery('TestQuery');
            mock.endpoint = baseURL + query;  
            mock.resource = r; 

            Test.setMock(HttpCalloutMock.class, mock); 
      
  
            Test.startTest();
                MyChainClass.dowork(); 
                Id apexJob = [SELECT Id FROM AsyncApexJob LIMIT 1].Id; 
                System.assert(apexJob != null, 'Job was null'); // Tests that the method ran. 
                System.abortJob(apexJob); // Abort job before test is over to avoid stack depth limit.
            Test.stopTest(); 
    
    }

Answers 2

  • Would you write the following unit test?

    @isTest static void test2Plus2() {
      System.assertEquals(4, 2 + 2, 'Expected 2 + 2 to equal 4');
    }
    

    If not, why not?

    ...

    That's right, we trust that the fundamental features of the language will always work as documented. Therefore, the following line of code is illogical:

    System.assert(apexJob != null, 'Job was null'); // Tests that the method ran. 
    

    There is no way to reach this code in a way where the assertion will fail. If the job failed to queue up, you would get a LimitException or something else, otherwise apexJob will not be null. At best, this code is deceptive, because it looks like an assertion, but it really may have just been written as:

    System.assert(true, 'We should reach this line of code');
    

    I realize that there's a "best practice" out there that says that all unit tests must have at least one assertion, and you can do that if you really want to, but there's no practical reason to assert anything here, just document that this unit test needs no assertions and move on.

    While you can abort the job, I typically find it better to avoid any side effects at all when I'm testing scheduled jobs, etc, and just perform a rollback:

    Test.startTest();
    SavePoint sp = Database.setSavePoint();
    MyChainClass.dowork(); 
    Database.rollback(sp); // Undoes everything from the setSavePoint call
    

    This is useful for everything, including dequeuing emails, scheduled jobs, batchable jobs, queueable jobs, and future methods.


  • My Answer

    The documentation on Salesforce recommends that we is Test.isRunningTest() But with this particular inner class set up it would not work as well and resulted in uncovered code. In this situation, we can handle segments of Queueable Apex that is called from a void method. However, there are problems if your method needs to assert the results of the Queueable Class that was enqueued. That said, if you just need to test that a Queueable class needs to be enqueued, you can use this approach to solve the problem maximums on stack depth.

    Here is how I had to implement @sfdcFox 's answer.

            Test.startTest();
                Savepoint sp = Database.setSavePoint(); 
                SandboxRefreshHelper.dowork(); 
                // Id apexJob = [SELECT Id FROM AsyncApexJob LIMIT 1].Id; 
                // System.assert(apexJob != null, 'Job was null'); 
                // System.abortJob(apexJob);
                Database.rollback(sp);  
            Test.stopTest(); 
    

    This provided the necessary means for getting the coverage on the method. It seems like both are valid options, but the previous answer is much more thorough in the mechanics of the test and should be seen as another option to get the same result.


Related Questions