TransWikia.com

Implementing test class for Queueable Apex that makes call outs

Salesforce Asked by Austin Evans on October 4, 2021

I have a Queueable class MyQueueableClass which in turns calls an outbound web service class.

public void execute(QueueableContext qc){
    try{
        GlobalCallout.makeCallout(caseId);
    } catch (Exception ex){
        System.debug('Exception: ' +ex);}
  }

And in class: GlobalCallout we are making a call out to an external service and performing other stuff. While writing a test class for the above Queueable Class, my call out is getting failed, as I am familiar with: We cannot perform an actual call out in test class. So, I picked a different route: Creating mock data for the class: GlobalCallout and right before the mock call out, I have System.enqueueJob(new MyQueueableClass(caseSC.Id)); assuming it would get the mock data from the context.

When I ran the test class, I get System.HttpResponse[Status=null, StatusCode=0]. Can someone tell me if I am doing it wrong? If my approach is not valid, can I get some insights on making it right?

My implementation for test class:

@isTest
public class MyQueueableTestClass{
static HttpResponse response;
Static String successData= 'Success';
Static String failureData= 'Faied';

@testSetup static void testData() { 

    //Create a test case record
}

public class MockSuccess implements HttpcalloutMock {
    public HTTPResponse respond(HTTPRequest req) {
                response = new HttpResponse();
                response.setbody(successData);
                response.setstatuscode(200);
                return response;
        }
}

static testmethod void testForSuccess(){
    Case caseSC = [Select Id From Case Limit 1];
    Test.setMock(HttpCalloutMock.class, new MockSuccess());
    Test.startTest();
    System.enqueueJob(new MyQueueableClass(caseSC.Id));
    System.debug('Response::'+response);
    Test.stopTest();
 }
}

I am getting the debug log as:

Response::null

Also, the debug for response within the web service call out class:

System.HttpResponse[Status=null, StatusCode=200]

I am sure, I might be missing something very small.

7 Answers

I'm not sure if something wrong in your code/org (didn't find any) or its core issue in SF related to Queueable & setMock.

The simple solution will be to use Test.isRunningTest() before your http.send. You can either construct dummy response there or you can use your mock class to get the test response. Probably something like:

HttpReponse res = Test.isRunningtest() ? new MockSuccess().response(req) : http.send(req);

Answered by Liron C on October 4, 2021

similar to this reference below can you try keeping your Test.setMock after Test.StartTest

Test.startTest();
Test.setMock(HttpCalloutMock.class, mock_obj);
// your method call
Test.stopTest();

FYR.. https://www.thephani.com/test-classes-for-future-methods-callout-true/

Answered by Rajesh Banglore on October 4, 2021

You need to define mock request & response to test all Callout related scenarios (200, 404, 406, 500 etc)

Implement HttpCalloutMock to determine what kind of response your code should get

@isTest
public class MyHttpMockClass implements HttpCalloutMock{
    // map<url, response>
    public Map<String, HttpResponse> responseMap;
    // map<url, true/false>, you only need to set true for urls where you want to test exception cases
    public Map<String, Boolean> sendExceptionMap;
    //add more functions/variables as per your requirement
    public HttpResponse respond(HttpRequest request){
        if(request.getEndPoint() == 'the endpoint i wanna test'){
            // use responseMap for cleaner code
            HttpResponse response = new HttpResponse();
            //create response body here or in a helper method
            response.setHeader('Content-Type', 'application/json');
            response.setBody('{"field":"value"}');
            response.setStatus('success');
            response.setStatusCode(200);
            return response;
            // you can create class level map to store responses for 200, 404, etc
            // and use that map & another variable to determine when to send what response
        }
    }
}

Set all these values before Test.startTest()

@isTest
public static void test_fn(){
  // some logic, or data creation
  MyHttpMockClass mobk_obj = new MyHttpMockClass();
  // set responses
  Test.startTest();
  Test.setMock(HttpCalloutMock.class, mock_obj);
  //you method call
  Test.stopTest();

  //Assert Results
}

Hope this helps.

Since a queueable class doesnt return values, you can't directly get HttpResponse by executing Queueable class. In a real-life scenario, a queueable class will make a callout, get response and update/create/delete a record. You can create response in such a way that the data is affected in an expected way. Then after Test.stopTest, you can assert that indeed target record(s) have been modified/created/deleted.

This way you can test callout that happens in a Queueable class

Answered by mritzi on October 4, 2021

A little addition to the recommendations of the previous responders:

  • Please, use assertions or debugs after Test.stopTest() since until it is called your enqueued job will not finish. In other words, the job hasn't finished yet at the line of your debug call.

I've faced the queueables that perform DMLs after the callout finishes, and about those ones, I can say that they worked correctly.

Answered by Jack Veromeev on October 4, 2021

Following points (specific to apex test class execution) might give you a better insight into what is going on here. Static variables in an apex test class are reset between:

  1. every test method execution.
  2. every transaction boundary (specific to the apex jobs or future calls inside test class).

Salesforce does this in order to maintain truly independent testing for each method or transaction boundary. I couldn't find out salesforce documentation on point #2 specific to the scenario you have mentioned, but this complies to the way async code is executed in apex. Also, I have tested out this scenario in the past, which led me to this understanding.

In your code, the Test.startTest() method starts an additional execution context (with a fresh set of limits) and apex test execution starts keeping a tab on all the async method calls. When Test.stopTest() is executed, it triggers all the async method calls or jobs to be executed synchronously. In this case, since it calls a queueable job (which I'm assuming in turn calls a future method), the code executes in its own transaction boundary. So, completion of this code execution would reset the static variable to the original context. I believe that the internal mechanism on how exactly this happens is something Salesforce should answer.

Now, in your code, the debug statement to check the response value is declared right before the Test.stopTest(). This implies that the test execution hasn't yet called the callout method i.e., the mock http respond method hasn't been called. So, the response value is null as declared in the original context. Even if you move this debug statement below the Test.stopTest(), you won't be able to check it's value because the test execution would have reset it to the original value.

Inside the web service call out class, you get the following response value because (in your mock http respond method) you are not setting the status value but only the statusCode.

System.HttpResponse[Status=null, StatusCode=200]

So, your mock http callout class and its methods are working fine, but the usage of static variables is not the right approach in this specific test assertion or verification (explicitly because the callout happens thru an queueable apex). If you were to test the same callout synchronously called via a separate class (just for testing purpose as I don't recommend it for production scenario), you will find that the static response variable retains the values, since the test execution stays within the same transaction boundary.

As others have pointed out,

  • Write test assertions based on the actual outcome and not the http response value.
  • Write Mock Http call out class as a separate class and not inside the test class (although the platform won't stop you from doing it). The purpose of this class is to mock the callout and not actually test the response status from the actual external service. So, as long as the correct parameters are passed to the Test.setMock method and the HttpResponse is correctly populated in the mock http callout implementation, the call out response would always be successful.

Hope this helps.

Answered by arut on October 4, 2021

It seems like you are implementing you Mock inside of the Test class itself. I would create it as another class instead, as @Aks is suggesting although it does not need to be global, but public. Also note that testMethod keyword is deprecated, you should use this instead:

@IsTest static void testForSuccess(){}

Additionally from my point of view (I do not know which logic does your main class involve) you should not check response status, but the logic that your class follows whether the statusCode and body are the expected or not.

Answered by Gabriel Serrano Salas on October 4, 2021

before running the test you need to call the Test.setMock() Do something like this

First create a httpmock class

global class YOURMOCKCLASS implements HttpCalloutMock
{
  global HttpResponse response(HttpRequest req)
    {
        HttpResponse res = new HttpResponse();
        res.setHeader({});
        res.setBody({});
        res.setStatusCode(200);
        return res;
    }

}

Then call that class from your test class.

Test.setMock(HttpCalloutMock.class, new YOURMOCKCLASS());
Test.startTest();
...
...
Test.stopTest();

Answered by Aks on October 4, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP