@future runs in test without Test.stopTest() - Can that be?
It sounds like a bug, or at least it is not documented; I would not depend on this behavior. If you want to test asynchronous code, use Test.stopTest(). This applies for Schedulable, Queueable, Batchable, and @future methods. If you're not using Test.stopTest(), then you're obviously not checking the results of the execution, which means you're only writing code coverage, not quality assurance test methods, which is literally the entire point of unit tests. I'll check with someone over at salesforce.com and see if something's changed. In the meantime, you should always use Test.stopTest() and verify the output of any asynchronous code to avoid regression and logic errors when deploying updates to your code. Also, using proper unit testing (by using asserts, for example) will help Salesforce prevent regression bugs when it runs the Apex Hammer Tests each release.
Here's proof that the methods are indeed executing:
@isTest class q173748 implements Database.Batchable<Integer>, Queueable, Schedulable {
@isTest static void testFuture() {
failAsync();
}
@isTest static void testBatchable() {
Database.executeBatch(new q173748());
}
@isTest static void testQueueable() {
System.enqueueJob(new q173748());
}
@isTest static void testSchedulable() {
System.schedule('q173748','0 0 0 * * ?', new q173748());
}
// ----------------------------------------------------------- //
public static void fail() {
System.assert(false);
}
@future static void failAsync() {
fail();
}
// ----------------------------------------------------------- //
public Integer[] start(Database.BatchableContext context) {
return new Integer[] { 1 };
}
public void execute(Database.BatchableContext context, Integer[] scope) {
fail();
}
public void finish(Database.BatchableContext context) {
}
// ----------------------------------------------------------- //
public void execute(QueueableContext context) {
fail();
}
// ----------------------------------------------------------- //
public void execute(SchedulableContext context) {
fail();
}
}
All of these tests fail, because asynchronous code is indeed called at the end of each unit test method.
Test.startTest()
makes all asynchronous calls synchronous (instant) in Test methods. It helps us in catch any result obtained by the execution of asynchronous calls.
Below is sample example:-
Test define One future and batch class:-
Future:
public class FutureCallClassMethod
{
@future
public static void futureCall()
{
Account acc = new Account(Name='Future Call');
insert acc;
System.debug(' Position 1: Account is created in Future: '+acc);
}
public static void BatchCall()
{
Database.executeBatch(new BatchClassSample());
System.debug(' Position 2: Batch executed');
}
}
Batch class:-
global class BatchClassSample implements Database.Batchable<sObject>{
global final String Query;
global final String Entity;
global final String Field;
global final String Value;
global BatchClassSample(){
Query='SELECT id FROM Account Limit 10';
}
global Database.QueryLocator start(Database.BatchableContext BC){
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext BC, List<sObject> scope) {
Account acc = new Account(Name='Batch Call');
insert acc;
System.debug(' ==> Position 1 in Batch class: '+scope);
}
global void finish(Database.BatchableContext BC){
}
}
Test class:-
@isTest
public class FutureCallClassMethodTest
{
public static testmethod void runFuture()
{
FutureCallClassMethod.futureCall();
List<Account> acc = [SELECT Id, Name from Account];
System.debug(' ==> Position 2: Future without Test.startTest: '+acc);
}
public static testmethod void runFutureWithTest()
{
Test.startTest();
FutureCallClassMethod.futureCall();
Test.stopTest();
List<Account> acc = [SELECT Id, Name from Account];
System.debug(' ==> Position 2: Future with Test.startTest: '+acc);
}
public static testmethod void runBatch()
{
Account accTest = new Account(Name='Test Account');
insert accTest;
FutureCallClassMethod.batchCall();
List<Account> acc = [SELECT Id, Name from Account WHERE Name!='Test Account'];
System.debug(' ==> Position 3: Batch without Test.startTest: '+acc);
}
public static testmethod void runBatchWithTest()
{
Account accTest = new Account(Name='Test Account');
insert accTest;
Test.startTest();
FutureCallClassMethod.batchCall();
Test.stopTest();
List<Account> acc = [SELECT Id, Name from Account WHERE Name!='Test Account'];
System.debug(' ==> Position 3: Batch with Test.startTest: '+acc);
}
}
When you execute the class you can clearly see that:-
Without
Test.startTest
, asynchronous code executes in last and the no account records was found when we queried that records in test class.With
Test.startTest
asynchronous operation executes in series and result also obtained in test class.
These can be seen in debugs as well. We get zero result of asynchronous operation without Test.StartTest
. And these executed in last.
Everything executes but their sequence changes.
If you look at the documentation for Test.stopTest()
, it says nothing about when asynchronous code does not run. Rather, it simply states that asynchronous is run synchronously when you call Test.stopTest()
.
Each test method is allowed to call this method only once. Any code that executes after the stopTest method is assigned the original limits that were in effect before startTest was called. All asynchronous calls made after the startTest method are collected by the system. When stopTest is executed, all asynchronous processes are run synchronously.
Nothing prevents the future from running. Introduce a long enough delay, and the future will have completed.
@IsTest
public class DemoFuture
{
@future
public static void createSomeData()
{
insert new Account(/*data*/);
}
static testmethod void testFutureCall()
{ // fail
createSomeData();
system.assertEquals(1, [SELECT count() FROM Account], 'Future not yet run');
}
static testmethod void testFutureCall()
{ // pass
createSomeData();
// introduce a delay
List<String> data = new List<String>();
for (Integer i = 0; i < 1000; i++) data.add('a'.repeat(1000));
for (Integer i = 0; i < 1000; i++)
data = (List<String>)JSON.deserialize(JSON.serialize(data), List<String>.class);
system.assertEquals(1, [SELECT count() FROM Account], 'Future should have run');
}
}