[SalesForce] How to test a Platform Event Trigger that makes callout(s)

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.

Best Answer

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.