1

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(); 
    
    }
2
  • 3
    We encourage you to answer your own questions, but you must formulate it as a Q&A. The Explanation part appears to be the answer to the problem. In order for your question to be formulated correctly, it must follow this style, or may be closed or deleted at the Community's/Moderator's discretion. Or, if you have a question about this is a good practice, or not, please edit your question so it appears as a question; right now, it reads like a statement.
    – sfdcfox
    Jul 2 '21 at 20:33
  • the unit test for doWork() should assert that a single AsyncApexJob was created where JobType = Queueable and ApexClass.Name = theQueueableClass; this confirms that the doWork() started a queueable
    – cropredy
    Jul 2 '21 at 21:40
5

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.

2
  • it seems like the difference between asserting 2+2 is 4 is that all those values are hardcoded. Doesn't querying to ensure that the job was queued different since there is actually a record dynamically produced?
    – thinker
    Jul 6 '21 at 14:39
  • 1
    @thinker The thing is, because System.enqueueJob is guaranteed to succeed, the assertion cannot fail, and an assertion that cannot fail is of little to no use. I'd only write this assertion if I felt that someone might remove the enqueueJob call, which would be rather silly given the nature of the method.
    – sfdcfox
    Jul 6 '21 at 15:26
0

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.

1
  • Trying to do the SavePoint creates an error. Savepoint was created during different phase of testing... going to try to work through this. But for now the answer I give provides the coverage needed on the method. Also seems like there is a difference between System.Assert(true,true); and Querying the AsyncJobs Object to ensure its created. Querying the Async Job after the AsyncJob has been inserted, seems more analogous to inserting contact records and querying them and asserting they were created.
    – thinker
    Jul 6 '21 at 14:44

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.