How to write test methods when Queueable class is called from the Batch Apex
Before we start discussing the solution lets revise some of the key points:
When Test.startTest() and Test.stopTest() are used then all the asynchronous requests between Test.startTest() and Test.stopTest() get executed synchronously, just after the execution of Test.stopTest().
We can create test data either in the Test Setup method() or inside our test method itself.
Data created in setup() Method is available for all the test methods for that given test class. But data created in a test method is available only in that test method and is not available in another test method. Though both test methods are in the same class.
1. First we are calling a Batch class from the test method without referring to the Queueable Class. Lets discuss 2 scenarios here
Without Test.startTest() & Test.stopTest()
With test.startTest() & Test.stopTest()
A. In the first scenario we are not using the Test.startTest() and Test.stopTest().
In the below test method usecase1() we are calling a Batch class.
TEST CLASS
@isTest
class MyTestClass {
@testSetup static void setup() {
System.debug('*** inside test Class setup()');
Contact rec = new Contact(lastName = 'Default-->');
insert rec;
}
@istest
static void usecase1(){
System.debug('*** inside test Class and usecase1 ');
System.debug('*** going to call Batch Class ');
Id jobId = Database.executeBatch(new MyBatchClass());
System.debug('*** inside test Class and call to Batch Class is done ');
Contact rec = [SELECT ID, lastName FROM Contact limit 1];
System.debug('*** inside test Class and final record name is '+rec.lastName);
System.assertEquals('Default--> Batch_Execute-> Batch_Finish', rec.lastName);
}
}
BATCH CLASS
public class MyBatchClass implements Database.Batchable<sObject>{
public Database.QueryLocator start(Database.BatchableContext bc) {
Database.QueryLocator result = Database.getQueryLocator('SELECT ID, lastname FROM Contact limit 1');
system.debug('*** inside BatchClass start() ');
return result;
}
public void execute(Database.BatchableContext bc, List<Sobject> records){
system.debug('*** inside Batch Class execute()');
Contact rec = [SELECT ID, lastname FROM Contact limit 1];
rec.lastname = rec.lastname + ' Batch_Execute->';
update rec;
}
public void finish(Database.BatchableContext bc){
Contact rec = [SELECT ID,lastname FROM Contact limit 1];
rec.lastname = rec.lastname + ' Batch_Finish';
update rec;
system.debug('*** inside Batch Class finish() ');
}
}
In the batch Class we are updating the contact last name with suffix ' Batch_Execute->'. In the next line of the Test Method, we are doing a query to get the latest contact details and using assert to check whether description is changed as expected or not. The expected result is ‘Default--> Batch_Execute-> Batch_Finish’.
Now lets execute the RUN TEST and see the result. we will get assertion failure as shown below:
RESULT: Assertion Failed: Expected: Default--> Batch_Execute-> Batch_Finish, Actual: Default-->
LOGS:
09:21:36:003 USER_DEBUG [4]|DEBUG|*** inside test Class setup()
09:21:36:081 USER_DEBUG [11]|DEBUG|*** inside test Class and usecase1
09:21:36:081 USER_DEBUG [13]|DEBUG|*** going to call Batch Class
09:21:36:108 USER_DEBUG [15]|DEBUG|*** inside test Class and call to Batch Class is done
09:21:36:118 USER_DEBUG [17]|DEBUG|*** inside test Class and final record name is Default-->
Actual Behaviour : Now lets try to understand the actual behaviour:
So we are executing the database.executeBatch() statement. As we know Batch job execution happens asynchronously. So it is adding one batch job to run in the queue which will get executed in near future.
When we are doing a query in the next line, it is returning the last Name of the contact as ‘Default🡪’ which was the initial value. Hence when we are trying to do the Assert in next line it is getting failed. Now lets see how can we get this fixed with the use of Test.starttest() and Test.stopTest().
In the second scenario we are using the Test.startTest() and Test.stopTest().
TEST CLASS
@isTest
class MyTestClass {
@testSetup static void setup() {
System.debug('*** inside test Class setup()');
Contact rec = new Contact(lastName = 'Default-->');
insert rec;
}
@istest
static void usecase1(){
System.debug('*** inside test Class and usecase1 ');
test.startTest();
System.debug('*** going to call Batch Class ');
Id jobId = Database.executeBatch(new MyBatchClass());
test.stopTest();
System.debug('*** inside test Class and call to Batch Class is done ');
Contact rec = [SELECT ID, lastName FROM Contact limit 1];
System.debug('*** inside test Class and final record name is '+rec.lastName);
System.assertEquals('Default--> Batch_Execute-> Batch_Finish', rec.lastName);
}
}
Now lets execute the RUN TEST and see the result.
RESULT: It worked as expected.
LOGS:
09:34:27:003 USER_DEBUG [4]|DEBUG|*** inside test Class setup()
09:34:27:072 USER_DEBUG [11]|DEBUG|*** inside test Class and usecase1
09:34:27:075 USER_DEBUG [14]|DEBUG|*** going to call Batch Class
09:34:27:150 USER_DEBUG [5]|DEBUG|*** inside BatchClass start()
09:34:27:181 USER_DEBUG [9]|DEBUG|*** inside Batch Class execute()
09:34:27:374 USER_DEBUG [18]|DEBUG|*** inside Batch Class finish()
09:34:27:383 USER_DEBUG [17]|DEBUG|*** inside test Class and call to Batch Class is done
09:34:27:388 USER_DEBUG [19]|DEBUG|*** inside test Class and final record name is Default--> Batch_Execute-> Batch_Finish
Actual behaviour:
Above we are executing the database.executeBatch() statement. As we know already, Batch job execution happens asynchronously. So it is getting added again in the queue which will get executed in near future.
Now we are executing the Test.stopTest(). All the asynchronous requests are run synchronously just after the execution of Test.stopTest(). This is the fact which solves the issue.
So now just after the execution of test.stopTest() line, batch job got run synchronously. Once its execution is completed, the next line got executed which is the query statement. This time we get the updated last name of the contact in the query and Assert
2. Now lets add the Queueable class call in the execute method of the Batch Class:
TEST Class
@isTest
class MyTestClass {
@testSetup static void setup() {
System.debug('*** inside test Class setup()');
Contact rec = new Contact(lastName = 'Default-->');
insert rec;
}
@istest
static void usecase1(){
System.debug('*** inside test Class and usecase1 ');
test.startTest();
System.debug('*** going to call Batch Class ');
Id jobId = Database.executeBatch(new MyBatchClass());
test.stopTest();
System.debug('*** inside test Class and call to Batch Class is done ');
Contact rec = [SELECT ID, lastName FROM Contact limit 1];
System.debug('*** inside test Class and final record name is '+rec.lastName);
System.assertEquals('Default--> Batch_Execute-> Queueable_Execute--> Batch_Finish', rec.lastName);
}
}
Batch Class:
public class MyBatchClass implements Database.Batchable<sObject>{
public Database.QueryLocator start(Database.BatchableContext bc) {
Database.QueryLocator result = Database.getQueryLocator('SELECT ID, lastname FROM Contact limit 1');
system.debug('*** inside BatchClass start() ');
return result;
}
public void execute(Database.BatchableContext bc, List<Sobject> records){
system.debug('*** inside Batch Class execute()');
System.debug('*** inside Batch Class and executing Queueable statement ');
System.enqueueJob(new MyQueueableClass());
Contact rec = [SELECT ID, lastname FROM Contact limit 1];
rec.lastname = rec.lastname + ' Batch_Execute->';
update rec;
System.debug('*** inside Batch Class and Queueable call is done ');
}
public void finish(Database.BatchableContext bc){
Contact rec = [SELECT ID,lastname FROM Contact limit 1];
rec.lastname = rec.lastname + ' Batch_Finish';
update rec;
system.debug('*** inside Batch Class finish() ');
}
}
QUEUEABLE CLASS
public class MyQueueableClass implements queueable, Database.Allowscallouts {
public void execute (QueueableContext context ){
system.debug('*** inside MyQueueableClass execute ');
Contact rec = [SELECT ID, lastname FROM Contact limit 1];
rec.lastname = rec.lastname + ' Queueable_Execute-->';
update rec;
}
}
Now lets run the Test Class.
Result: As Expected, lastname is updated to ‘Default+Batch_Execute+Queueable_Execute+Batch_Finish’
Logs:
09:51:45:003 USER_DEBUG [4]|DEBUG|*** inside test Class setup()
09:51:45:093 USER_DEBUG [11]|DEBUG|*** inside test Class and usecase1
09:51:45:095 USER_DEBUG [14]|DEBUG|*** going to call Batch Class
09:51:45:171 USER_DEBUG [5]|DEBUG|*** inside BatchClass start()
09:51:45:205 USER_DEBUG [9]|DEBUG|*** inside Batch Class execute()
09:51:45:205 USER_DEBUG [10]|DEBUG|*** inside Batch Class and executing Queueable statement
09:51:45:255 USER_DEBUG [15]|DEBUG|*** inside Batch Class and Queueable call is done
09:51:45:280 USER_DEBUG [4]|DEBUG|*** inside MyQueueableClass execute
09:51:45:354 USER_DEBUG [21]|DEBUG|*** inside Batch Class finish()
09:51:45:363 USER_DEBUG [17]|DEBUG|*** inside test Class and call to Batch Class is done
09:51:45:368 USER_DEBUG [19]|DEBUG|*** inside test Class and final record name is Default--> Batch_Execute-> Queueable_Execute--> Batch_Finish
Actual Behaviour: So here is the explanation:
When we call the Queueable from the batch execute () method then the execution goes like this.
<CODE_UNIT_STARTED for test Class setup() method>
< /CODE_UNIT_ENDED for test Class setup() method>
<CODE_UNIT STARTED for test class method1()>
<System.Test.startTest() method execusion will start>
</System.Test.startTest() method execusion will end>
<database.executebatch() statement will get executed>
< System.Test.stopTest() method execution will start>
<CODE_UNIT gets STARTED for start() method of the Batch class>
</CODE_UNIT gets ENDED for start() method of the Batch class>
<CODE_UNIT gets STARTED for execute() method of the Batch class>
<Calling Queueable Statement Starts>
</Calling Queueable Statement Ends>
</CODE_UNIT gets ENDED for start() method of the Batch class>
<CODE_UNIT gets STARTED for execute() method of the Queueable class>
</ CODE_UNIT gets Ended for execute() method of the Queueable class >
<CODE_UNIT gets STARTED for finish() method of the Batch class>
</CODE_UNIT gets ENDED for finish() method of the Batch class>
</ System.Test.stopTest() method execution will End>
<Debug statement after the stop.test() in test method Starts>
</Debug statement after the stop.test() in test method Ends>
</ CODE_UNIT ENDED for test class method1()>