Read more at jamessimone.net, 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:

classes/Crud_Tests.cls
1link@isTest

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 }

13link}

That leads to this familiar assertion failure:

1linkSystem.AssertException:

2linkAssertion Failed:

3linkSame value: null

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

classes/Crud.cls
1linkpublic class Crud {

2link public SObject doInsert(SObject record) {

3link Database.insert(record);

4link return record;

5link }

6link}

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

classes/Crud.cls
1link public SObject doInsert(SObject record) {

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

3link }

4link

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!

classes/Crud_Tests.cls
1link//in Crud_Tests.cls

2link@isTest

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>();

14link

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);

19link

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

21link contacts.add(c);

22link records.add(c);

23link }

24link

25link insert accounts;

26link insert contacts;

27link

28link try {

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

30link } catch(Exception ex) {

31link System.assert(false, ex);

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

33link }

34link}

That brings about this lovely exception:

1linkSystem.AssertException:

2linkAssertion Failed: System.TypeException:

3linkCannot have more than 10 chunks in a single operation.

4linkPlease 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:

classes/Crud.cls
1linkpublic SObject doInsert(SObject record) {

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

3link}

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

5link this.sortToPreventChunkingErrors(records);

6link Database.insert(records);

7link return records;

8link}

9link

10linkpublic SObject doUpdate(SObject record) {

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

12link}

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

14link this.sortToPreventChunkingErrors(records);

15link Database.update(records);

16link return records;

17link}

18link

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 }

26link}

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

2link@testSetup

3linkprivate static void setup() {

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

5link}

6link

7link@isTest

8linkstatic void it_should_do_crud_upsert() {

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

10link contact.FirstName = 'Harry';

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

12link

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

14link}

15link

16link//and in Crud.cls

17linkpublic SObject doUpsert(SObject record) {

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

19link}

20link

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

22link this.sortToPreventChunkingErrors(records);

23link Database.upsert(records);

24link return records;

25link}

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:

classes/ICrud.cls
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);

11link

12link void doDelete(SObject record);

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

14link void doHardDelete(SObject record);

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

16link}

Implementing this in the base class is trivial:

classes/Crud.cls
1linkpublic virtual class Crud implements ICrud {

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

3link}

And now for my next trick ...

classes/CrudMock.cls
1link//@isTest classes cannot be marked virtual

2link//bummer

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>();

9link

10link //prevent undue initialization

11link private CrudMock() {}

12link

13link private static CrudMock thisCrudMock;

14link

15link //provide a getter for use

16link public static CrudMock getMock() {

17link if(thisCrudMock == null) {

18link thisCrudMock = new CrudMock();

19link }

20link

21link return thisCrudMock;

22link }

23link

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 ...

31link}

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;

3link

4link//in CrudMock.cls

5linkpublic static RecordsWrapper Inserted {

6link get {

7link return new RecordsWrapper(InsertedRecords);

8link }

9link }

10link

11link public static RecordsWrapper Upserted {

12link get {

13link return new RecordsWrapper(UpsertedRecords);

14link }

15link }

16link

17link public static RecordsWrapper Updated {

18link get {

19link return new RecordsWrapper(UpdatedRecords);

20link }

21link }

22link

23link public static RecordsWrapper Deleted {

24link get {

25link return new RecordsWrapper(DeletedRecords);

26link }

27link }

28link

29link public static RecordsWrapper Undeleted {

30link get {

31link return new RecordsWrapper(UndeletedRecords);

32link }

33link }

34link

35link public class RecordsWrapper {

36link List<SObject> recordList;

37link RecordsWrapper(List<SObject> recordList) {

38link this.recordList = recordList;

39link }

40link

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

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

43link }

44link

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

46link

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

48link

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

50link

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

52link

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

54link

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 }

64link

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 }

74link

75link public Integer size() {

76link return this.recordList.size();

77link }

78link

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 }

87link

88link public SObject firstOrDefault {

89link get {

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

91link return recordList[0];

92link }

93link return null;

94link }

95link }

96link

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 Force.com 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!)

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

Mocking DML through CRUD wrappersImplementing the DML interface

Home 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 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 Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests



Read more tech articles