Read more at, or return to the homepage

Mocking DML

I never met a unit test I didn't like. - Me, probably.

Hi everyone and welcome back to the Joys of Apex. This time around, we'll be covering the all-important subject of unit testing. In the Introduction we covered TDD as it relates to Salesforce, and some early on project experience I had as a Salesforce developer that impressed upon me the need to have a snappy test suite:

I think we'd all like to ship features quickly and safely at the end of the day.

linkMocking DML through CRUD wrappers

To improve on the abysmal testing time of my first project, I began writing some failing tests:


2linkprivate class Crud_Tests {

3link @isTest

4link static void it_should_insert() {

5link Contact con = new Contact(

6link LastName = 'Washington',

7link LeadSource = 'test'

8link //other fields you require

9link );

10link new Crud().doInsert(con);

11link System.assertNotEquals(null, con.Id);

12link }


That leads to this familiar assertion failure:


2link$Assertion Failed:

3link$Same value: null

Ouch. OK! Let's implement the method in our Crud class to fix this issue.

1linkpublic class Crud {

2link public SObject doInsert(SObject record) {

3link Database.insert(record);

4link return record;

5link }


Easy peasy. In fact, let's update the code so that we get both record and records-based ability to insert:

1link public SObject doInsert(SObject record) {

2link return this.doInsert(new List<SObject>{ record })[0];

3link }


5link public List<SObject> doInsert(List<SObject> records) {

6link Database.insert(records);

7link return records;

8link }

You can imagine the implementation for the update, upsert, and delete methods ... but there's one gotcha!

1link//in Crud_Tests.cls


3linkstatic void it_should_not_fail_on_update_due_to_chunking_errors() {

4link /*this constant is normally kept in a class I call SalesforceLimits

5link thanks to Xi Xiao for pointing out that though the salesforce

6link error message returns "Cannot have more than 10 chunks in a single operation"

7link if the objects are always alternating,

8link the chunk size increases with each alternation, thus the error will occur

9link in as little as 6 iterations */

10link Integer MAX_DML_CHUNKS = 6;

11link List<SObject> records = new List<SObject>();

12link List<Account> accounts = new List<Account>();

13link List<Contact> contacts = new List<Contact>();


15link for(Integer i = 0; i < MAX_DML_CHUNKS; i ++) {

16link Account a = new Account(Name = 'test' + i);

17link accounts.add(a);

18link records.add(a);


20link Contact c = new Contact(LastName = test + i);

21link contacts.add(c);

22link records.add(c);

23link }


25link insert accounts;

26link insert contacts;


28link try {

29link new Crud().doUpdate(records);

30link } catch(Exception ex) {

31link System.assert(false, ex);

32link //should not make it here ...

33link }


That brings about this lovely exception:


2link$Assertion Failed: System.TypeException:

3link$Cannot have more than 10 chunks in a single operation.

4link$Please rearrange the data to reduce chunking.

When developing in Apex, I think people quickly come to learn that no matter how much you know about the SFDC ecosystem, there are always going to be new things to learn. Getting burned is also sometimes the fastest way to learn. I'm not an oracle — this test is really a regression test, which only came up during active development on my second Salesforce org when our error logs started occasionally recording this error.

Let's fix the chunking issue:

1linkpublic SObject doInsert(SObject record) {

2link return this.doInsert(new List<SObject>{record})[0];


4linkpublic List<SObject> doInsert(List<SObject> records) {

5link this.sortToPreventChunkingErrors(records);

6link Database.insert(records);

7link return records;



10linkpublic SObject doUpdate(SObject record) {

11link return this.doUpdate(new List<SObject>{record})[0];


13linkpublic List<SObject> doUpdate(List<SObject> records) {

14link this.sortToPreventChunkingErrors(records);

15link Database.update(records);

16link return records;



19linkprivate void sortToPreventChunkingErrors(List<SObject> records) {

20link //prevents a chunking error that can occur

21link //if SObject types are in the list out of order.

22link //no need to sort if the list size is below the limit

23link if(records.size() >= SalesforceLimits.MAX_DML_CHUNKING) {

24link records.sort();

25link }


And now the tests pass — one gotcha down! I feel ready to take on the world! Just kidding...

There's always one more gotcha in Apex (and gotchas = n + 1 is only true with the gotchas I know). Let's cover one more ... lovely ... issue:

1link//in Crud_Tests.cls


3linkprivate static void setup() {

4link insert new Contact(FirstName = 'George');




8linkstatic void it_should_do_crud_upsert() {

9link Contact contact = [SELECT Id FROM Contact];

10link contact.FirstName = 'Harry';

11link new Crud().doUpsert(contact);


13link System.assertEquals('Harry', contact.FirstName);



16link//and in Crud.cls

17linkpublic SObject doUpsert(SObject record) {

18link return this.doUpsert(new List<SObject>{ record })[0];



21linkpublic List<SObject> doUpsert(List<SObject> records) {

22link this.sortToPreventChunkingErrors(records);

23link Database.upsert(records);

24link return records;


Prior to Summer '20, this would have led to the error System.TypeException: DML on generic List<SObject> only allowed for insert, update or delete. Thankfully, a hacky workaround for generically spinning up strongly-typed SObject lists is no longer necessary. Many thanks to Brooks Johnson for pointing this out in the comments!

Moving on to seperating concerns in our production code ...

linkImplementing the DML interface

In order to make use of this Crud class within our production level code while keeping our tests blazing fast, we're going to need a common interface:

1linkpublic interface ICrud {

2link SObject doInsert(SObject record);

3link List<SObject> doInsert(List<SObject> recordList);

4link SObject doUpdate(SObject record);

5link List<SObject> doUpdate(List<SObject> recordList);

6link SObject doUpsert(SObject record);

7link List<SObject> doUpsert(List<SObject> recordList);

8link List<SObject> doUpsert(List<SObject> recordList, Schema.SObjectField externalIDField);

9link SObject doUndelete(SObject record);

10link List<SObject> doUndelete(List<SObject> recordList);


12link void doDelete(SObject record);

13link void doDelete(List<SObject> recordList);

14link void doHardDelete(SObject record);

15link void doHardDelete(List<SObject> recordList);


Implementing this in the base class is trivial:

1linkpublic virtual class Crud implements ICrud {

2link //you've already seen the implementation ...


And now for my next trick ...

1link//@isTest classes cannot be marked virtual


3linkpublic virtual class CrudMock extends Crud {

4link public static List<SObject> InsertedRecords = new List<SObject>();

5link public static List<SObject> UpsertedRecords = new List<SObject>();

6link public static List<SObject> UpdatedRecords = new List<SObject>();

7link public static List<SObject> DeletedRecords = new List<SObject>();

8link public static List<SObject> UndeletedRecords = new List<SObject>();


10link //prevent undue initialization

11link private CrudMock() {}


13link private static CrudMock thisCrudMock;


15link //provide a getter for use

16link public static CrudMock getMock() {

17link if(thisCrudMock == null) {

18link thisCrudMock = new CrudMock();

19link }


21link return thisCrudMock;

22link }


24link // DML

25link public override List<SObject> doInsert(List<SObject> recordList) {

26link TestingUtils.generateIds(recordList);

27link InsertedRecords.addAll(recordList);

28link return recordList;

29link }

30link // etc ...


A couple of things to note here:

1link//in a test class looking to get ONLY an inserted Task record

2linkTask t = (Task) CrudMock.Inserted.Tasks.singleOrDefault;


4link//in CrudMock.cls

5linkpublic static RecordsWrapper Inserted {

6link get {

7link return new RecordsWrapper(InsertedRecords);

8link }

9link }


11link public static RecordsWrapper Upserted {

12link get {

13link return new RecordsWrapper(UpsertedRecords);

14link }

15link }


17link public static RecordsWrapper Updated {

18link get {

19link return new RecordsWrapper(UpdatedRecords);

20link }

21link }


23link public static RecordsWrapper Deleted {

24link get {

25link return new RecordsWrapper(DeletedRecords);

26link }

27link }


29link public static RecordsWrapper Undeleted {

30link get {

31link return new RecordsWrapper(UndeletedRecords);

32link }

33link }


35link public class RecordsWrapper {

36link List<SObject> recordList;

37link RecordsWrapper(List<SObject> recordList) {

38link this.recordList = recordList;

39link }


41link public RecordsWrapper ofType(Schema.SObjectType sObjectType) {

42link return new RecordsWrapper(this.getRecordsMatchingType(recordList, sObjectType));

43link }


45link public RecordsWrapper Accounts { get { return this.ofType(Schema.Account.SObjectType); }}


47link public RecordsWrapper Leads { get { return this.ofType(Schema.Lead.SObjectType); }}


49link public RecordsWrapper Contacts { get { return this.ofType(Schema.Contact.SObjectType); }}


51link public RecordsWrapper Opportunities { get { return this.ofType(Schema.Opportunity.SObjectType); }}


53link public RecordsWrapper Tasks { get { return this.ofType(Schema.Task.SObjectType); }}


55link public Boolean hasId(Id recordId) {

56link Boolean exists = false;

57link for(SObject record : this.recordList) {

58link if(record.Id == recordId) {

59link exists = true;

60link }

61link }

62link return exists;

63link }


65link public Boolean hasId(Id whatId, SObjectField idField) {

66link Boolean exists = false;

67link for(SObject record : this.recordList) {

68link if((Id)record.get(idField) == whatId) {

69link exists = true;

70link }

71link }

72link return exists;

73link }


75link public Integer size() {

76link return this.recordList.size();

77link }


79link public SObject singleOrDefault {

80link get {

81link if(recordList.size() > 1) {

82link throw new Exceptions.InvalidOperationException();

83link }

84link return recordList.size() == 0 ? null : recordList[0];

85link }

86link }


88link public SObject firstOrDefault {

89link get {

90link if(recordList.size() > 0) {

91link return recordList[0];

92link }

93link return null;

94link }

95link }


97link public List<SObject> getRecordsMatchingType(List<SObject> records, Schema.SObjectType sObjectType) {

98link List<SObject> matchingRecords = new List<SObject>();

99link for (SObject record : records) {

100link if(record.getSObjectType() == sObjectType) {

101link matchingRecords.add(record);

102link }

103link }

104link return matchingRecords;

105link }

106link }

Yeah. That's some boilerplate right there. In practice, the RecordWrapper helper for the CrudMock came into being only when we realized as a team that we were repetitively trying to filter records out of the static lists implemented in the CrudMock. And that's another important part of practicing TDD correctly: there's a reason I didn't lead with the ICrud interface when beginning this discussion. That would have been a "prefactor," or premature optimization. It wasn't relevant to the subject material at hand.

Try to avoid the urge to prefactor in your own Apex coding practice, and (when possible) encourage the same in your teammates. TDD at its best allows you (and a friend, if you are doing extreme / paired programming) to extract design elements and shared interfaces from your code as you go, as a product of making the tests pass. Some of the best code I've written on the platform was the result of refactors — made possible by excellent unit tests, and the organic need to revisit code.

I've worked in orgs where you had to swim through layer after layer of abstraction to get to any kind of implementing code. In my experience, over-architecting code leads to unnecessary abstraction and terrible stacktraces. Maintaining the balance between code reusability and readability is of course a life-long see-saw.

Thanks for tuning in for another Joys Of Apex talk — I hope this post encourages you to think outside the box about how to extract the database from impacting your SFDC unit test time. Next time around, we'll cover some important bridging ground — now that you've got a DML wrapper for your Apex unit tests, how do you begin to enforce the usage of the actual Crud class in production level code while ensuring that whenever mocking is necessary in your tests, you can easily swap out for the CrudMock? The answer lies in everyone's favorite Gang Of Four pattern - the Factory pattern. (If you just read that and winced ... you truly have my apologies!)

For another example of taking this pattern and using it out in the wild, check out my post on building a custom rollup solution -- the tests shown exhibit how powerful (and how powerfully time-saving) it can be to mock calls to the database.

The original version of Mocking DML can be read on my blog.

Mocking DML through CRUD wrappersImplementing the DML interface

Home Advanced Logging Using Nebula Logger Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Formula Date Issues Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Replacing DLRS With Custom Rollup Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions The Tao Of Apex Writing Performant Apex Tests

Read more tech articles