Mocking DML
Posted: December 28, 2019

Mocking DML

Table of Contents:

    • Mocking DML through CRUD wrappers
    • Implementing the DML interface
  • Postscript

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:

  • it encouraged quick iteration
  • fast tests meant as feature requests rolled in and existing code was refactored, the team remained confident that regressions were being avoided
  • fast deploys meant shipping features faster

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

Mocking DML through CRUD wrappers

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

@IsTest
private class DMLTests {
  @IsTest
  static void it_should_insert() {
    Contact con = new Contact(
      LastName = 'Washington',
      LeadSource = 'test'
      // other fields you require
    );
    new DML().doInsert(con);
    System.assertNotEquals(null, con.Id);
  }
}

That leads to this familiar assertion failure:

System.AssertException:
Assertion Failed:
Same value: null

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

public class DML {
  public SObject doInsert(SObject record) {
    Database.insert(record);
    return record;
  }
}

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

// in DML.cls
  public SObject doInsert(SObject record) {
    return this.doInsert(new List<SObject>{ record })[0];
  }

  public List<SObject> doInsert(List<SObject> records) {
    Database.insert(records);
    return records;
  }

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

// in DMLTests.cls
@IsTest
static void it_should_not_fail_on_update_due_to_chunking_errors() {
  /*
  this constant is normally kept in a class I call SalesforceLimits
  thanks to Xi Xiao for pointing out that though the salesforce
  error message returns "Cannot have more than 10 chunks in a single operation"
  if the objects are always alternating,
  the chunk size increases with each alternation, thus the error will occur
  in as little as 6 iterations */
  Integer MAX_DML_CHUNKS = 6;
  List<SObject> records = new List<SObject>();
  List<Account> accounts = new List<Account>();
  List<Contact> contacts = new List<Contact>();

  for(Integer i = 0; i < MAX_DML_CHUNKS; i ++) {
    Account a = new Account(Name = 'test' + i);
    accounts.add(a);
    records.add(a);

    Contact c = new Contact(LastName = test + i);
    contacts.add(c);
    records.add(c);
  }

  insert accounts;
  insert contacts;

  try {
    new DML().doUpdate(records);
  } catch(Exception ex) {
    System.assert(false, ex);
    // should not make it here ...
  }
}

That brings about this lovely exception:

System.AssertException:
Assertion Failed: System.TypeException:
Cannot have more than 10 chunks in a single operation.
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:

// in DML.cls
public SObject doInsert(SObject record) {
  return this.doInsert(new List<SObject>{record})[0];
}
public List<SObject> doInsert(List<SObject> records) {
  this.sortToPreventChunkingErrors(records);
  Database.insert(records);
  return records;
}

public SObject doUpdate(SObject record) {
  return this.doUpdate(new List<SObject>{record})[0];
}
public List<SObject> doUpdate(List<SObject> records) {
  this.sortToPreventChunkingErrors(records);
  Database.update(records);
  return records;
}

private void sortToPreventChunkingErrors(List<SObject> records) {
  // prevents a chunking error that can occur
  // if SObject types are in the list out of order.
  // no need to sort if the list size is below the limit
  if(records.size() >= SalesforceLimits.MAX_DML_CHUNKING) {
    records.sort();
  }
}

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:

// in DMLTests.cls
@TestSetup
private static void setup() {
  insert new Contact(FirstName = 'George');
}

@IsTest
static void it_should_do_crud_upsert() {
  Contact contact = [SELECT Id FROM Contact];
  contact.FirstName = 'Harry';
  new DML().doUpsert(contact);

  System.assertEquals('Harry', contact.FirstName);
}

// and in DML.cls
public SObject doUpsert(SObject record) {
  return this.doUpsert(new List<SObject>{ record })[0];
}

public List<SObject> doUpsert(List<SObject> records) {
  this.sortToPreventChunkingErrors(records);
  Database.upsert(records);
  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 …

Implementing the DML interface

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

public interface IDML {
  SObject doInsert(SObject record);
  List<SObject> doInsert(List<SObject> recordList);
  SObject doUpdate(SObject record);
  List<SObject> doUpdate(List<SObject> recordList);
  SObject doUpsert(SObject record);
  List<SObject> doUpsert(List<SObject> recordList);
  List<SObject> doUpsert(List<SObject> recordList, Schema.SObjectField externalIDField);
  SObject doUndelete(SObject record);
  List<SObject> doUndelete(List<SObject> recordList);

  void doDelete(SObject record);
  void doDelete(List<SObject> recordList);
  void doHardDelete(SObject record);
  void doHardDelete(List<SObject> recordList);
}

Implementing this in the base class is trivial:

public virtual class DML implements IDML {
  // you've already seen the implementation ...
}

And now for my next trick …

// @IsTest classes cannot be marked virtual
// bummer
public virtual class DMLMock extends DML {
  public static List<SObject> InsertedRecords = new List<SObject>();
  public static List<SObject> UpsertedRecords = new List<SObject>();
  public static List<SObject> UpdatedRecords = new List<SObject>();
  public static List<SObject> DeletedRecords = new List<SObject>();
  public static List<SObject> UndeletedRecords = new List<SObject>();

  // prevent non-singleton initialization
  private DMLMock() {
  }

  private static DMLMock thisDMLMock;

  // provide a getter for use
  public static DMLMock getMock() {
    if(thisDMLMock == null) {
        thisDMLMock = new DMLMock();
    }

    return thisDMLMock;
  }

  public override List<SObject> doInsert(List<SObject> recordList) {
    TestingUtils.generateIds(recordList);
    InsertedRecords.addAll(recordList);
    return recordList;
  }

  // etc ...
}

A couple of things to note here:

  • Insert / Upsert methods stub out Ids using a helper method. Since it’s common in tests to confirm record insertion by positing the existence of an SObject’s Id, stubbing Ids helps to decouple our tests from the mock DML implementation.
  • @IsTest classes cannot be marked virtual. You could make the argument that true test safety could be achieved by simply reimplementing the IDML interface instead of extending the existing DML class. I’ve gone back and forth on this many times.
  • Within the DMLMock, we also ended up implementing some helpful getter methods for retrieving records of a specific Type. If you’re testing a multi-step process with a lot of DML along the way, it can be helpful to pull back only the records you need to assert against. I’ll use an example with Tasks below.
// in a test class looking to get ONLY an inserted Task record
Task t = (Task) DMLMock.Inserted.Tasks.singleOrDefault;

// in DMLMock.cls
public static RecordsWrapper Inserted {
  get {
    return new RecordsWrapper(InsertedRecords);
  }
}

public static RecordsWrapper Upserted {
  get {
    return new RecordsWrapper(UpsertedRecords);
  }
}

public static RecordsWrapper Updated {
  get {
    return new RecordsWrapper(UpdatedRecords);
  }
}

public static RecordsWrapper Deleted {
  get {
    return new RecordsWrapper(DeletedRecords);
  }
}

public static RecordsWrapper Undeleted {
  get {
    return new RecordsWrapper(UndeletedRecords);
  }
}

public class RecordsWrapper {
  private final List<SObject> recordList;
  public RecordsWrapper(List<SObject> recordList) {
    this.recordList = recordList;
  }

  public RecordsWrapper ofType(Schema.SObjectType sObjectType) {
    return new RecordsWrapper(this.getRecordsMatchingType(recordList, sObjectType));
  }

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

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

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

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

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

  public Boolean hasId(Id recordId, Schema.SObjectField fieldToken) {
    Boolean exists = false;
    for(SObject record : this.recordList) {
      if(record.get(fieldToken) == recordId) {
        exists = true;
        break;
      }
    }
    return exists;
  }

  public Boolean hasIdField(Id whatId, SObjectField idFieldToken) {
    return this.hasId(whatId, idFieldToken);
  }

  public hasId(Id searchId) {
    return this.hasId(
      searchId,
      searchId.getSObjectType().getDescribe().fields.getMap().get('Id')
    );
  }

  public List<SObject> Records {
    get {
      return this.recordList;
    }
  }

  public Boolean isEmpty() {
    return this.recordList.isEmpty();
  }

  public Integer size() {
    return this.recordList.size();
  }

  // singleOrDefault throws for empty; firstOrDefault does not
  public SObject singleOrDefault {
    get {
      if (recordList.size() > 1) {
        throw new Exceptions.InvalidOperationException();
      }
      return recordList.size() == 0 ? null : recordList[0];
    }
  }

  public SObject firstOrDefault {
    get {
      return recordList.size() > 0 ? recordList[0] : null;
    }
  }

  public List<SObject> getRecordsMatchingType(List<SObject> records, Schema.SObjectType sObjectType) {
    List<SObject> matchingRecords = new List<SObject>();
    for (SObject record : records) {
      if (record.getSObjectType() == sObjectType) {
        matchingRecords.add(record);
      }
    }
    return matchingRecords;
  }
}

Yeah. That’s some boilerplate right there. In practice, the RecordWrapper helper for the DMLMock 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 DMLMock. And that’s another important part of practicing TDD correctly: there’s a reason I didn’t lead with the DML 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 lifelong see-saw.

PS — I did some stress testing on the use of the DMLMock / DML class I am recommending versus the FFLib Apex Mocks library which was developed by Andrew Fawcett, who worked on FinancialForce prior to working for Salesforce. FinancialForce’s approach closely aligns with that of Mockito, one of the pre-eminent Java mocking solutions, and as such is widely accepted in the industry as the de-facto way to approach mocking within Apex. I will also be covering this in a future post, but for now if you are curious, check out the project on my Github for a sneak peek of the relative performance merits for each library. Cheers!

Postscript

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 DML class in production level code while ensuring that whenever mocking is necessary in your tests, you can easily swap out for the DMLMock? The answer lies in everyone’s favorite Gang Of Four pattern - the Factory pattern.

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. As well, there’s Mocking Apex History Records for a powerful take on when the DAO pattern can come into play in conjunction with mocking.

In the past three years, hundreds of thousands of you have come to read & enjoy the Joys Of Apex. Over that time period, I've remained staunchly opposed to advertising on the site, but I've made a Patreon account in the event that you'd like to show your support there. Know that the content here will always remain free. Thanks again for reading — see you next time!