Batchable class which publishes events… test assert not working

apexbatchunit-test

I have a batchable class which updates a record, then publishes an event.

To write a test which ensures the "publish" occurs, I've created a fake instance of the class which normally undertakes the publish, but simply increments a counter. However- although the counter gets incremented, my assert still sees the default value of 0.

I'm sure I'm missing something simple as we have other similar tests, but this one has me stumped today!

Below is a simplified version of the class and the test- debug shows that in my "CapturingEventSink" that's passed in, the publishCallCount does get incremented, however in the final assert, it's 0 and the test fails.
I've tried various combinations of test.starttest / test.stoptest and adding/removing the eventbus.deliver (which I think is irrelevant here as there's no "real" eventbus happening)

Simple version of the class being tested:

global class TestBatchable implements Database.Batchable<SObject>, Database.AllowsCallouts, Database.RaisesPlatformEvents
{
    private final IMyEventSink m_EventSink;
    private final List<SObject> m_ObjectsToUpdate;

    public TestBatchable(List<SObject> objectsToUpdate,
                                      IMyEventSink myEventSink)
    {
        m_ObjectsToUpdate = objectsToUpdate;
        m_EventSink = myEventSink;
    }

    public List<SObject> start(Database.BatchableContext context)
    {
        return m_ObjectsToUpdate;
    }

    public void execute(Database.BatchableContext context, List<zqu__Quote__c> scope)
    {
        List<SObject> objectsToExpire = new List<SObject>();
        for (SObject q : scope)
        {
            q.somefield__c = true;
            objectsToExpire.add(q);
        }

        update objectsToExpire;

        for(SObject q : objectsToExpire)
        {
            System.debug('publishing event');
            m_EventSink.publish(new MySuperEvent__e(TheId = q.Id));
        }
    }

    public void finish(Database.BatchableContext context)
    {

    }
}

… and the test class:

@IsTest
public class TestBatchableTest
{
    @IsTest
    public static void jobIsQueued()
    {
        SObject q = new SObject();
        q.Name = 'TestObj';
        q.someField__c = false;

        insert q;

        CapturingEventSink mockEventSink = new CapturingEventSink();

        TestBatchable recalculator = new TestBatchable(new List<SObject>{q}, mockEventSink);

        Test.startTest();

        Database.executeBatch(recalculator, 10);

        Test.stopTest();

        SObject updatedObj = [SELECT Id, someField__c
                                      FROM SomeObject
                                      WHERE Id = :q.Id];

        System.assertEquals(true, updatedObj.someField__c, 'Object not expired');
        System.assertEquals(1, mockEventSink.publishCallCount, 'Expected number of events not enqueued');
    }

    public class CapturingEventSink implements IMyEventSink
    {
        public Integer publishCallCount = 0;
        public List<MySuperEvent__e> argPublishedEvents = new List<MySuperEvent__e>();

        public void publish(MySuperEvent__e event)
        {
            publishCallCount++;
            argPublishedEvents.add(event);
            System.debug('publishCallCount is now ' + publishCallCount); //shows 1
        }
    }
}

Best Answer

Personally, I do not believe your event sink should be stateful. So the change I would make is to structure it as static. I usually prefer a @TestVisible static void setMock to override singletons for dependency injection, as it keeps a close parallel to out of the box callout mocking.

static MyService instance = new MyService();
@TestVisible static void setMock(MyService mock) { instance = mock; }

If you just make this change and adjust your test accordingly, it should work. But if you really want the event sink to be stateful to the batch for some reason, you need to add Database.Stateful to your list of implementations.

Related Topic