Skip to content
Home » Blog » Units of Fun

Units of Fun

TL; DR;

Peeping into the Unit of Work to help with Batch job chaining and having to really think about who extends who when mocking and testing.

More posts about UoW

UoW and SObject Instances

I previously talked about the need to be careful about how you register records as dirty with the Apex Common’s Unit of Work. There is another little gotcha to be ware of I recently ran into. Though when it does…

DmlWork a.k.a. the UoW meets Triggers

TL; DR; Building a neat UoW wrapper for compounding standalone service methods effectively and merging the Unit of Work approach together with updating SObject instances in Triggers. Full code on GitHub. Overloaded Often I need to call the same logic…

Problem

The Unit of Work is perhaps the easiest concept from the Enterprise Patterns to grasp and adopt (maybe after Selectors actually). I’m definitely a fan. It was always ugly and clunky having to return the records to be saved back up the call tree. And when more SObjectTypes have to be handled even worse. Recently though I ran into a situation where I needed to know the changed records – in a batch.

The job was fed with records of Type A, but actually acting on records of Type B. When my batch was done, I needed to run another one (you guessed it) to further process the records of Type B that were changed. Only a minority of the records in scope caused any changes so it would have been very inefficient to just look at all B records related to all A records.

Peeping Inside

The UoW is not really public about what it has inside (kinda the point usually) but it’s very much ready to be extended. So I went ahead and added a couple of read only properties. 

public inherited sharing class UnitOfWork extends fflib_SObjectUnitOfWork {
   public UnitOfWork(List<SObjectType> sObjectList) {
       super(sObjectList);
   }
 
   public List<Id> allNewOrUpdatedIds {
       get {
           if (allNewOrUpdatedIds == null) {
               allNewOrUpdatedIds = new List<Id>();
               for (List<SObject> newRecords : this.m_newListByType.values()) {
                   for (Sobject newRecord : newRecords) {
                       allNewOrUpdatedIds.add(newRecord.Id);
                   }
               }
               for (Map<Id, SObject> updatedRecords : this.m_dirtyMapByType.values()) {
                   allNewOrUpdatedIds.addAll(updatedRecords.keySet());
               }
           }
           return allNewOrUpdatedIds;
       }
       private set;
   }
 
   public List<Id> newOrUpdatedIds(List<SObjectType> objectTypes) {
       List<Id> recordIds = new List<Id>();
       for (SObjectType objectType : objectTypes) {
           String objectName = objectType.getDescribe().getName();
           if (this.m_newListByType.containsKey(objectName)) {
               for (Sobject newRecord : this.m_newListByType.get(objectName)) {
                   recordIds.add(newRecord.Id);
               }
           }
           if (this.m_dirtyMapByType.containsKey(objectName)) {
               recordIds.addAll(this.m_dirtyMapByType.get(objectName).keySet());
           }
       }
       return recordIds;
   }
 
   public override void onCommitWorkFinishing() {
       this.allNewOrUpdatedIds = null; //force re-calc
   }
}

Now.. this feels very dirty and it should! I don’t think it’s a very good idea to use this in most of the applications of the UoW concept. It’s harmless to any code using UoW (as long as it remains read only), i.e. it’s not going to have side effects sent down. But it is peeping into details I was not meant to know or care about (after all that’s one of the reasons I’m using UoW in the first place) and that could be introducing some very hard to spot dependencies. For the specific purpose I am looking at though, I think it’s fine. Or at least an acceptable trade-off. You be the judge 🙂

How to control that it’s only getting used for this (or similar) means? We could agree on that and take our word for it. Or maybe create a separate layer in the Application / Class Factory (or whatever we use to get UoW instances) that’s going to be easier to spot in reviews.

And this is the jist of my batch using the modified UoW.

public with sharing class ExampleBatch implements Database.Batchable<HeaderRecord__c>, Database.Stateful {
   private Date fromDate;
   private Date toDate;
   private Set<Id> updatedChildRecords;
 
   public ExampleBatch(Date fromDate, Date toDate) {
       this.fromDate = fromDate;
       this.toDate = toDate;
       this.updatedChildRecords = new Set<Id>();
   }
 
   public Iterable<HeaderRecord__c> start(Database.BatchableContext bc) {
       HeaderRecordSelector records = HeaderRecordSelector.newInstance();
       return (Iterable<HeaderRecord__c>) records.locatorByDateRange(this.fromDate, this.toDate);
   }
 
   public void execute(Database.BatchableContext bc, List<SObject> scope) {
       UnitOfWork uow = (UnitOfWork) ClassFactory.newUnitOfWorkInstance();
       try {
           HeaderRecordService service = HeaderRecordService.newInstance();
           service.ensureChildRecordsUpToDate((List<HeaderRecord__c>) scope, uow);
           uow.commitWork();
           this.updatedChildRecords.addAll(uow.getNewOrUpdatedIds(new List<SObjectType>{ Schema.ChildRecord__c.SObjectType }));
       } catch (Exception e) {
           Logger.error(scope, e);
           throw e;
       } finally {
           Logger.insertLogs();
       }
   }
 
   public void finish(Database.BatchableContext bc) {
       if (!this.updatedChildRecords.isEmpty()) {
           Database.executeBatch(new PotentialChildBatch(updatedChildRecords));
       }
   }
}

So that’s it, job done right? Of course not. 

Needing to Mock

When building the test to cover the batch, I don’t want to be testing the service nor the selector providing data, right? Just the whole orchestration bit it’s in charge of. So I mock. I mock the Selector and Service and because of that I mock the UoW too, because of course it can’t actually commit. No data exists => it’s likely to fail.

And there’s the issue. The fflib__SObjectMocks.SObjectUnitOfWork cannot be cast into my new UoW. Easy solution, right? Obvious almost. Just create an extension of that too, adding support for the new methods.

Well, it’s not really that simple. Remember that we’re talking about a class that’s not really meant to be returning anything back up the way. So the mock UoW is not really built to maintain a state (i.e. remember the records that were put in) in the same way the real one is. Even if I add the new methods to an extension I don’t really have where to get the Ids from.

Simple Alternative

Luckily for me I had previously created my own Mock Unit of Work. A few months back, when I was still too afraid to dive into ApexMocks. It’s designed to remember the records just like the real UoW and when commitWork() is called it just moves them from one list to another. I’ve used it in my tests to assert that the right records were being created.

...
public void commitWork() {
    for (SObject o : this.registeredInsertedRecords) {
        o.Id = TestUtilities.getFakeId(o.getSObjectType());
        this.insertedRecords.put(o.Id, o);
    }
    this.registeredInsertedRecords.clear();
    this.updatedRecords.putAll(registeredUpdatedRecords);
    this.registeredUpdatedRecords.clear();
    this.deletedRecords.putAll(registeredDeletedRecords);
    this.registeredDeletedRecords.clear();
}
...

The big benefit I saw in it was that I could have sort of “hybrid” tests. We are only slowly refactoring our big org and it’s not always possible to mock everything. With this I can have some real data, mock what I can and then in the end have the mock UoW avoid at least the last trip to trigger land.

Of course I could do that with the ApexMock UoW too in some ways. I know. Told you I wasn’t ready, didn’t I?

Almost There

There is one more trick needed. I can’t have the mock extend fflib_ISObjectUnitOfWork like I originally did, because then it wouldn’t have the new methods (I didn’t extend the real UoW because I didn’t want to have to override all the methods it has). My solution is to extend the fflib interface with my own and then have my adjusted UoW and Mock UoW both implement that.

public interface IUnitOfWork extends fflib_ISObjectUnitOfWork {
    List<Id> getNewOrUpdatedIds();
    List<Id> getNewOrUpdatedIds(List<SObjectType> objectTypes);
}

Most of the code can still work against the original interface so as not to have access to the potentially problematic new methods. Special cases work with the additional layer. Everybody’s happy.

Unit Test

Finally this is the test.

Let’s not worry too much about the Class Factory or Logger. The selector returning Iterator is also not relevant (although very interesting – being able to mock Database.QueryLocator for the batch start method).

Instead let’s look at the VoidAnswerUpdateItem class allowing for the fake assignment of Ids into the UoW. This makes this test really only test the actual orchestration of Batch classes. With real Apex Jobs being created without any data setup needed.

@IsTest
private class ExampleBatchTest {
   @IsTest
   static void whenItemUpdatedPreInvoiceBatchIsLaunchedForItsCampaign() {
       List<HeaderRecord__c> headerRecords = new List<HeaderRecord__c>{
           TestFactory.getRecord(Schema.HeaderRecord__c.SObjectType), //fake
           TestFactory.getRecord(Schema.HeaderRecord__c.SObjectType)
       };
 
       fflib_ApexMocks mocks = new fflib_ApexMocks();
       fflib_ISObjectUnitOfWork unitOfWorkMock = new MockUnitOfWork(); //not from ApexMocks!
       HeaderRecordSelector selectorMock = (HeaderRecordSelector) mocks.mock(HeaderRecordSelector.class);
       HeaderRecordService serviceMock = (HeaderRecordService) mocks.mock(HeaderRecordService.class);
 
       mocks.startStubbing();
       mocks.when(selectorMock.sObjectType()).thenReturn(Schema.HeaderRecord__c.SObjectType);
       mocks.when(selectorMock.locatorByDateRange(startOfTheMonth, endOfTheMonth)).thenReturn(headerRecords);
       ((HeaderRecordService) mocks.doAnswer(new VoidAnswerUpdateItem(), serviceMock))
           .ensureChildRecordsUpToDate(headerRecords, unitOfWorkMock);
       mocks.stopStubbing();
 
       ClassFactory.setMock(HeaderRecordSelector.class, selectorMock);
       ClassFactory.setMock(HeaderRecordService.class, serviceMock);
       ClassFactory.setMock(fflib_ISObjectUnitOfWork.class, unitOfWorkMock);
 
       Test.startTest();
       Database.executeBatch(new ExampleBatch(startOfTheMonth, endOfTheMonth));
       Test.stopTest();
 
       List<AsyncApexJob> childRecordsJob = [SELECT Id, Status FROM AsyncApexJob WHERE ApexClass.Name = 'PotentialChildBatch'];
       System.assertEquals(1, childRecordsJob.size(), 'childRecordsJob job should have been queued for the updated records');
   }
 
   private class VoidAnswerUpdateItem implements fflib_Answer {
       public Object answer(fflib_InvocationOnMock invocation) {
           List<HeaderRecord__c> headerRecords = (List<HeaderRecord__c>) invocation.getArgument(0);
           fflib_ISObjectUnitOfWork uow = (fflib_ISObjectUnitOfWork) invocation.getArgument(1);
           for (HeaderRecord__c headerRecord : headerRecords) {
               uow.registerUpsert(
                   new ChildRecord__c(
                       Id = fflib_IDGenerator.generate(Schema.ChildRecord__c.SObjectType, HeaderRecord__c = headerRecord.Id)
                   )
               );
           }
           return null; // answer must return something
       }
   }
}

And that is finally it. Well done for making it all the way here. Hope you had as much fun as I did. You can find the above code here as well, although this time it’s not really fully deployable. You’ll need to deploy Apex Mocks and Apex Common first and then create the 2 used SObjects too 😉

It’s never too late to start playing with the Enterprise Patterns. I think every Salesforce Developer needs to get to know them at some point. I’m doing my best and I keep finding out just how good the Apex Common package really is.

Where to next

Marek Tyrlík

Marek Tyrlík

Sep 22, 20226 min read

TL; DR; Marek shows you can re-train to be a Salesforce Developer successfully from a completely unrelated field. Even then…

DML (Mock) Service

DML (Mock) Service

Apr 6, 20226 min read

TL; DR; Apex utility wrapping DML operations and logging their results consistently. And a mock version of the same allowing…

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x