0

I'm trying to run this test and it keeps failing. The error I'm getting is "System.QueryException: unexpected token: 'null'" so it's related to my lstContacts SOQL query, but I can't figure out what's going. Works fine when I'm running the callout in the execute anonymous window and elsewhere.

Callout:

public class FilastCallout implements Schedulable {
    
    public void execute(SchedulableContext ctx) {
        makePostCallout();
    }
    
    @future(callout=true) public static void makePostCallout() {
        
        
        // Get custom settings
        Filast_Settings__c fu = Filast_Settings__c.getOrgDefaults();
        
        List<Contact> lstContacts = Database.query('SELECT '+fu.Contact_First_Name__c+','+fu.Contact_Last_Name__c+','+fu.Contact_Email__c+',AccountId,Account.'+fu.Account_Name__c+' FROM Contact WHERE '+fu.Contact_Email__c+' !=null AND '+fu.Contact_First_Name__c+' !=null');
        
        // Transform list ot json and add advanced settings
        String jsonS = JSON.serialize(new Map<String, Object> {
            'min_contacts' => fu.Minimum_Contacts__c,
            'pct_syntax' => fu.Minimum_Pct_Same_Syntax__c,
            'acct_fuzzy' => fu.Account_Fuzzy_Match__c,
            'contacts' => lstContacts
        });
        
        // Create api request and send jsonified data
        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('https://j9cf5hum55.execute-api.us-east-1.amazonaws.com/default/Filast');
        request.setMethod('POST');
        request.setHeader('Content-Type', 'application/json');
        // Set the body as a JSON object
        request.setBody(jsonS);
        HttpResponse response = http.send(request);
        
        // Convert json response back to list of accounts
        JSONParser parser = JSON.createParser(response.getBody());
        
        List<Account> lstAccounts = new List<Account>();
        
        while (parser.nextToken() != null) {
            if (parser.getCurrentToken() == JSONToken.START_OBJECT) {
                    Account a = (Account)parser.readValueAs(Account.class);
                    lstAccounts.add(a);
                }
        }
        
        //Update accounts
        update lstAccounts;
        
    }
}

Mock:

@isTest
public class CalloutMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        
            // Create a fake response
            HttpResponse response = new HttpResponse();
            response.setHeader('Content-Type', 'application/json');
            response.setBody('[{"Id": "0015f000004htPSAAY", "Filast_Email_Syntax__c": "[email protected]"}, {"Id": "0015f000004htPTAAY", "Filast_Email_Syntax__c": "[email protected]"}]');
            response.setStatusCode(200);
            return response;
    }
}

Test Class:

@isTest
public class FilastCalloutTest {

    @isTest
    public static void makePostCalloutTest() {
        Test.setMock(HttpCalloutMock.class, new CalloutMock());
        Test.startTest();
          FilastCallout.makePostCallout();
        Test.stopTest();
    }
}
4
  • Please edit your question to add how it is failing (e.g., exact error or failure text). (Please read the What topics can I ask about here? section of Help Center where, among other things, it says that your question should include, 'A specific, detailed description of the issue ("it doesn’t work" is not enough to allow the community to help).')
    – Moonpie
    Commented Jul 16, 2022 at 0:59
  • @Moonpie sorry about that. I've added some more detail.
    – JVZ
    Commented Jul 16, 2022 at 2:46
  • @JVZ I have one scheduler apex, one flow, and one apex to make integration, the scheduler calls the flow, and the flow calls the apex of integration, in this case, should give an error of "Callout from scheduled Apex not supported"? Commented Sep 21, 2022 at 18:40
  • 1
    @ArthurAlmeida I recommend googling your error message and, if you don't find an answer, creating another question in StackExchange. It's difficult to troubleshoot w/out your code, but I think you might need to use the future method. See this thread: salesforce.stackexchange.com/questions/344564/…
    – JVZ
    Commented Sep 29, 2022 at 23:04

1 Answer 1

3

tl;dr:

  • You need to explicitly create data for your test (Sobjects like Account, and Custom Settings)
  • You shouldn't hard-code Ids anywhere in a test (so you need to adjust your HttpCalloutMock class)
  • You should use JSON.deserialize() instead of using JSONParser

The longer version

In unit testing, we can generally cause one (and only one) level of asynchronous code to be executed.

When I say "asynchronous code", I mean one of "Schedulable", "Queueable", "@future", or "batchable".

In unit testing, we shouldn't be concerned about how the asynchronous code is run. It's something we don't have control over. We should trust that Salesforce is handling it appropriately (Salesforce should be the ones writing the tests for that feature).So in general, we should be calling the asynchronous code ourselves, directly, when running unit tests.

In that regard, it looks to me like you're doing that appropriately. We can't call @future methods directly (and cause them to run synchronously without using Test.startTest() and Test.stopTest()), but you are calling the method as directly as you can. I might suggest breaking out the actual work into a different static method (which your @future method, or any other async code, would call), but that's not usually a big issue until you have multiple levels of async code (async code which calls other async code).

The issue that I see here is that you aren't doing any data setup in your test. That, and you're hard-coding an Id in your callout mock.

Tests generally have 3 phases:

  1. Set up test data (set up the "test environment"), a.k.a. "Arrange"
  2. Cause the code you want to test to be executed, a.k.a. "Act"
  3. Gather the results, and compare them to expected values, a.k.a. "Assert"

In tests, you generally don't have access to data (tests are isolated from "real" data, and real data is isolated from tests), except for "setup objects" like User, Profile, Custom Metadata Type records, and a handful of other things. If you need data inside of a test, you need to create that data in the test class.

Notably, Custom Settings (both List and Hierarchy types) are not setup objects. You'll need to create and insert that data in your test, along with at least one Account and one Contact (presumably related to your test Account) so that your query has some data to return, and the Contact has some data for your later DML statement to update the Account(s).

You'll want to modify your HttpCalloutMock implementation to be able to take (and only use) Account Ids from the Account(s) that you create as part of the test.

There is a way to use "real" data in tests, but it's generally not advised (unless you absolutely need it, which you don't in this case). Ids are effectively guaranteed to be different between each of your sandboxes, and between any given sandbox and your "production" org (the exception being full and partial copy sandboxes). Hard-coding Ids and using "real" data makes your tests brittle and likely to fail when you try to run them in other orgs.

As a final note, you generally don't want to be using JSONParser. It's very verbose, prone to (programmer) errors, and we have a better way to deal with JSON. You should either create an Apex class (or classes) to mimic the structure of the JSON you're expecting (which, by the look of it, is entirely possible in your case) so you can use JSON.deserialize(jsonString, apexClassType), or use JSON.deserializeUntyped() to get something that you can cast to a Map<String, Object> or a List<Object>.

4
  • As a side note: Performing DML on Custom Settings is one of the things that is well known to cause tests to run slow (or at least slower than they otherwise could) and annihilates your ability to run tests in parallel (useful for testing within an org, though tests are always run synchronously for deployments). Having a proxy class that allows you to add test records for Custom Settings (and using the proxy instead of using the Custom Setting directly) is something I'd recommend, but that's a bit more of an advanced topic and not really relevant to the question being asked.
    – Derek F
    Commented Jul 16, 2022 at 3:09
  • Thank you for the thorough explanation! You were correct - I needed to create the contact, accounts, and settings. To solve the hard coding Ids issue, I just removed the setBody part of my mock HttpResponse and my test ran fine. Is that the right solution?
    – JVZ
    Commented Jul 16, 2022 at 14:46
  • @JohnvanZalk 1/2: It's a solution, but I wouldn't recommend it. Code coverage for tests is mostly a side-effect of testing. The real value of tests for us as developers is to show that the code behaves as expected. By removing the setBody(), you're effectively cutting the update lstAccounts; out of your test. The account update is definitely something you want to make an assertion for (it's something that is being changed as a result of running your code). I'd suggest making a constructor for your CalloutMock and passing it the JSON that you want to return from the callout.
    – Derek F
    Commented Jul 16, 2022 at 16:05
  • 2/2: The idea being that you have access to the Id of the test Account(s) that you're creating. You also have per-test (we generally want multiple test methods, each one testing a different scenario. Test enough scenarios, and you naturally get high coverage) control over what data is returned from your callout. Being able to define the callout data per-test makes it pretty easy to write assertions (after running the code to be tested, query the Accounts, and assert that the field that your test response should have updated was in fact updated, and to the appropriate value).
    – Derek F
    Commented Jul 16, 2022 at 16:12

You must log in to answer this question.

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