0

I have built an Apex integration with a customer's custom API. All API responses are of the form:

{
    "code": 3000,
    "message": "SUCCESS",
    "data": { "some object": "JSON data" }
}

When there is an error, the code value is something other than 3000, and "message" contains the error message.

As part of the integration, we will have a daily Scheduled job which makes a simple API call as a "heartbeat" job. It looks like this:

public with sharing class APIPlatformHeartbeat implements Schedulable, Database.AllowsCallouts {

    public void execute(SchedulableContext ctx) {
        callApi();
    }

    public static void runNow() { // for ad-hoc runs
        callApi();
    }

    // @Future required to make callout from scheduled (non-batch) job
    @Future(Callout=true)
    static void callApi() {
        LogUtil logger = new LogUtil();
        try {
            API_Platform_Integration_Settings__c settings = API_Platform_Integration_Settings__c.getOrgDefaults();
            APIPlatform.Connection conn = APIPlatform.connect(settings.Named_Credential__c, logger);
            conn.getGranularities();
        }
        catch (APIPlatform.APIPlatformException e) {
            logger.logError(e.getMessage(), e.response);
            throw e;
        }
        catch(Exception e) {
            logger.logException(e);
            throw e;
        }
        finally {
            logger.publish();
        }
    }

}

Note that that call to conn.getGranularities() will throw an APIPlatform.APIPlatformException if the API response contains an non-3000 response code. This method handles that and logs the exception if needed. But it's not working when I test it. Here's the unit test:

@IsTest
static void testRunNow_Fail() {
    TestObjectFactory.mockSingleResource('api_error');
    Test.startTest();
    APIPlatformHeartbeat.runNow();
    Test.stopTest();
    List<Error_Log__c> res = [SELECT Id, Message__c FROM Error_Log__c WHERE Type__c = :LogUtil.LOG_TYPE_ERROR];
    System.assertEquals(1, res.size());
    System.assertEquals('An internal service error occurred.', res[0].Message__c);
}

The call to TestObjectFactory.mockSingleResource('api_error'); just mocks the response with a static resource that contains a simple error response. This should cause the exception to be thrown, then caught in the future method, and logged. However, when I run the unit test, the test dies with:

12:07:24.4 (93391748)|FATAL_ERROR|APIPlatform.APIPlatformException: An internal service error occurred.

That exception is a custom exception, so should be catchable. I'm guessing this has something to do with future context, but I'm not aware of any changes to exception handling in future methods.

2
  • 3
    Looks to me like you're re-throwing the exceptions in APIPlatformHeartbeat after catching them. That'll cause the exception to bubble up until it reaches your test method (and then your test dies because of the uncaught, re-thrown exception). Is the re-throwing of exceptions a behavior that you want/need to keep?
    – Derek F
    Commented Aug 23, 2022 at 16:38
  • @DerekF good catch - those rethrows were added before I realized I needed to make the callout method @future to place nice with Schedulable. No longer needed with the logging in place, and I completely overlooked it. Please add as an answer so I can accept. Commented Aug 23, 2022 at 16:46

1 Answer 1

2

APIPlatformHeartbeat.callApi() is currently re-throwing the exceptions after they're being caught (and handled).

If you don't have anything else further up the chain to catch these exceptions, then it'll ultimately bubble up to your test method and cause the test to fail (due to the uncaught exception).

If you don't need those exceptions to be re-thrown, then you can simply delete the throw e; lines and that should be the end of it.

If you do need those exceptions to be re-thrown, then you should add a try/catch to your test methods (inside of startTest/stopTest? just outside of startTest/stopTest? not really sure which) so the exception is caught.

For testing purposes, it may make sense to re-throw the exceptions and then explicitly catch them in your test (so you're testing for the exception itself rather than bringing a dependency on your logger into this test and trying to decipher the log). Wrapping the throw e; inside of Test.isRunningTest() and then using the following in your test method(s)

Boolean caughtException = false;
try{
    Test.startTest();
    APIPlatformHeartbeat.runNow();
    Test.stopTest();
}catch(APIPlatform.APIPlatformException e){
    caughtException = true;
}catch(Exception e){
    // No work done, just swallow the exception
    // This is a "Pokemon catch" so you can control the error message if another exception is thrown.
}finally{
    System.assert(caughtException, 'Did not catch the expected exception');
}

You must log in to answer this question.

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