2

I am integrating with a third-party API that calls an unauthenticated webhook (HTTP Post) that I have implemented with a small payload; my code then uses that payload to make an authenticated call to the remote API to fetch the sensitive data associated with the notification. For various reasons, we have implemented the inbound HTTP Post by publishing platform events. There is a trigger that fires on the event; it is responsible for making the callout to the remote API and taking action on the result.

I can test all of the pieces in separation, but when I attempt to test the Trigger by publishing an event, I get a System.CalloutException: Callout from triggers are currently not supported error, even though I have mocked the HTTP request. I know the mocking works, as I have other tests which use the same mocking setup and call the Trigger handler directly. I'm guessing that when the EventBus fires the event in test context, the Trigger must run in a different context which does not see the HttpCalloutMock that I've setup. And yes, I have wrapped the EventBus.publish() call in Test.startTest()/Test.stopTest(), and I have tried manually delivering the event with Test.getEventBus().deliver(). Here's my current test method:

@IsTest
static void testTrigger() {
    setupMocks(); // used by all test which make callouts
    MY_Platform_Event__e evt = new MY_Platform_Event__e(
        EventType__c = 1,
        PackageId__c = '717cda19-b348-447c-96b4-018ac806be13',
        MessageId__c = '717cda19-b348-447c-96b4-018ac806be13\\1',
        Description__c = 'Package.Create',
        Environment_Token__c = ENV_TOKEN
    );
    Test.startTest();
    Database.SaveResult sr = EventBus.publish(evt);
    System.assertEquals(true, sr.isSuccess());
    Test.getEventBus().deliver();
    Test.stopTest();
}

I really only need the required 1% code coverage for my trigger, as all of the handler logic is tested elsewhere in isolation. Because the whole point of the Trigger is to call the remote API, however, I can't construct even a minimum test that will successfully fire the trigger, unless I add an Test.isRunningTest() check inside the Trigger handler. I can do so if needed, but the need for such a test is a red flag to me that I need to check my assumptions. Is it possible to test a Platform Event Trigger that makes an HTTP callout, without using Test.isRunningTest() to skip the callout?

edit to add: upon reflection, I can't even use Test.isRunningTest() in the handler, since the handler is called directly in other tests. I can either check Test.isRunningTest() directly in the Trigger, or use a @TestVisible private Boolean flag in the handler to allow a test method to turn off the callout code... neither of which approaches do I like.

2
  • I'm confused - triggers can't do callouts synchronously so if you have a trigger on My_Platform_Event__e, it needs to delegate to an async transaction the actual callout.
    – cropredy
    Commented Jan 20, 2020 at 19:30
  • @cropredy You are correct of course; I've posted an answer explaining my mistake and the fix in more detail. If you'd like to post an answer as well, you'll get my vote :) Commented Jan 21, 2020 at 15:06

2 Answers 2

1

Apex triggers can't do synchronous callouts. This includes Apex triggers that subscribe to/consume Platform Events.

It doesn't really matter (in the testmethod) whether the platform events are delivered by:

  • Test.getEventBus().deliver() or
  • Test.stopTest() without the deliver()

because the exception is caused by a trigger doing a synchronous callout.

You need to delegate the callouts to one of the Apex async feature: future, queueable, batchable, schedulable. Which you choose will determine how your testmethod is written.

2

As @cropredy pointed out in a comment, I cannot actually make a callout from the trigger. The error message I quoted in the question clearly stated "System.CalloutException: Callout from triggers are currently not supported", but I think I was mentally mis-reading that as "Callouts not allowed in Test Methods", which I'd already dealt with in a couple of tests when I forgot to call my callout-mocking setup method.

As stated in the Developer's Guide overview page on Triggers:

Callouts must be made asynchronously from a trigger so that the trigger process isn’t blocked while waiting for the external service's response.

What threw me off is the fact that Apex Triggers on Platform Events are already asynchronous; as stated in the Platform Events Developer Guide:

Unlike triggers on standard or custom objects, triggers on platform events don’t execute in the same Apex transaction as the one that published the event. The trigger runs asynchronously in its own process under the Automated Process entity.

But I had missed this additional information further down the page:

Event triggers have many of the same limitations of custom and standard object triggers. For example, with some exceptions, you generally can’t make Apex callouts from triggers.

I assume that "with some exceptions" is an allusion to the use of future methods. I refactored my Event Trigger Handler to pass the payload info to a future handler, which eliminated the error, however, I wasn't seeing the future method getting invoked. I believe that Test.stopTest() was causing the Event to be delivered, which enqueued the future method, which was never invoked because Test.stopTest() had already run. I added a call to Test.getEventBus().deliver() before the call to Test.stopTest(), (which I had tried before and removed at some point before switching to a future method); this allowed the event to be delivered and the future method enqueued in advance of the Test.stopTest() call. This allowed everything to be tested end-to-end.

You must log in to answer this question.

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