1

To start, let's say I have this simple unit test:

@isTest
public static void shouldCallFutureMethod() {
    Test.startTest();
    System.enqueueJob(myQueueableJob);
    Test.stopTest();
} 

Given certain conditions, when I run myQueueableJob.execute(), then the execute() function will in turn call a separate function which is annotated with @Future because it makes a callout. Let's call that @Future function makeCallout().

I understand that in the above unit test, I cannot run any asserts that depend on the behavior of the makeCallout() function. That's fine. What I want to do is assert that the makeCallout() function has been queued, and assert the state of the parameters with which it was called. For example, if makeCallout() has a parameter X, I want to do the following:

@isTest
public static void shouldCallFutureMethod() {
    Test.startTest();
    System.enqueueJob(myQueueableJob);
    Test.stopTest();
    Assert.isTrue(/* makeCallout(X) was called once */);
    Assert.areEqual(SOME_NUMBER, X);
} 

In a different programming language, I might write an interface that defines the makeCallout() function then implement that interface in a test class that simply stores X so I can check its value in the unit test. Unfortunately, since a @Future function has to be static in Apex, I can't define the function in an interface and override it in an implementation.

Additionally, I've seen that the AsyncApexJob table is supposed to have information about the async jobs that have been queued, but when I've tried querying it after the Test.stopTest() call, I noticed that all of the MethodName fields of each record in that table are null. So I can't assert which method was called. Furthermore, I don't see any way to access the parameters with which it was called.

Can anyone help? The specific restrictions of Apex seem like they are designed in such a way that will prevent me from writing a real test for this functionality.

2 Answers 2

1

Static variables persist across an entire unit test, so one easy design is:

public class Utils {
  @TestVisible static Boolean makeCalloutCalled = false;
  @future(callout=true) public static void makeCallout() {
     // Do your callout, then...
     makeCalloutCalled = true;
  }
}

And then your test checks for this:

@isTest
public static void shouldCallFutureMethod() {
    Test.startTest();
    System.enqueueJob(myQueueableJob);
    // Don't forget to make a HttpCalloutMock...
    setupMock();
    Test.stopTest();
    Assert.isTrue(Utils.makeCalloutCalled);
} 
0

Writing a test to make sure that a certain method is called strikes me as fragile, though I suppose you may be doing an integration/functional/end-to-end test.

I do see a way around your predicament, but I'd first like to suggest some alternatives

  • Queueable classes can make callouts (you'd need implements Queueable, Database.AllowsCallouts in your class declcaration), so you may not need the separate @future method
  • Instead of enqueuing your queueable class, call the execute() method directly and pass null in for the QueueableContext
    • The queuing mechanism is not something you control, so it's not something we should generally test. That's Salesforce's responsibility
    • This is also generally helpful for testing other async mechanisms like Batchable and Schedulable Apex
    • ...and for shortening a chain of callouts by 1 (since we can only enqueue one job in a given test method)

If you're really stuck on the sync -> queueable -> future route, then the way to make the @future injectable is to simply call it from an instance method. E.g.

public interface InjectableFuture {
    public void runFuture();
}
public class SomeFuture implements InjectableFuture {
    @future(callout=true)
    public static void makeCallout() {
    }

    public void runFuture() {
        SomeFuture.makeCallout();
    }
}
public class MyQueueable implements Queueable {
    private InjectableFuture futureDelegate;

    public MyQueueable(InjectableFuture givenDelegate) {
        // null coalescing operator introduced in API v60.0 (Spring '24)
        futureDelegate = givenDelagate ?? new SomeFuture();
    }

    public void execute(QueueableContext ctx) {
        futureDelegate.runFuture();
    }
}
@isTest
private class SomeTest {
    static Integer futureCallsMade = 0;

    // Inner class for injection
    private class FutureTest implements InjectableFuture {
        public void runFuture() {
            futureCallsMade++;
        }
    }

    @isTest
    static void executeQueueable_CallsFuture() {
        // inject the test implementation into your queueable
        MyQueueable sut = new MyQueueable(new FutureTest());

        ...
    }
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .