ApexMocks, Selectors, and Formula Fields

This is part 3 of a series, see Part 1 and Part 2

If you’ve bought into mocking Sobjects for use in Unit Tests, you probably have run into the roadblock wherein formula fields and system audit fields can’t be created in SObjects. For example, these won’t compile:

 Account acct = new Account(LastModifiedDate = System.now(), Name = 'Foo');

 Opportunity oppo = new Opportunity(HasLineItems = true, ...);

An example
So, if you have a class method-under-test that wants to do work on Account Opportunities and the work varies based on the value of Opportunity.FormulaField__c

public void doSomeWork(set<ID> acctIds) {
   Integer count = 0;
   for (Account acct : [select Id, 
                            (select Id, FormulaField__c from Opportunities)
                            from Account where Id IN: acctIds])
     for (Opportunity o : acct.Opportunities)
        if (oppo.FormulaField__c == 'foo') doSomeFooWork();
        else doSomeBarWork();

As a good practitioner of Enterprise Patterns, you convert the code to use a Selector:

public void doSomeWork(set<ID> acctIds) {
   Integer count = 0;
   for (Account acct : AccountsSelector.newInstance()
                            .selectWithOpposById(acctIds))
     for (Opportunity o : acct.Opportunities)
       if (oppo.FormulaField__c == 'foo') doSomeFooWork();
       else doSomeBarWork();

I’m assuming you know how to create selectors and the corresponding entry in Application.cls.

The testmethod using ApexMocks

So, let’s set up a typical ApexMocks testmethod where we mock the AccountsSelector.

@isTest private static void testDoSomeWork() {
  fflib_ApexMocks mocks = new fflib_ApexMocks();

  // Given a mock selector
  AccountsSelector mockAcctsSelector = (AccountsSelector)
      mocks.mock(AccountsSelector.class);

  // Given a mock Selector with stubbed results for FormulaField__c
  mocks.startStubbing();
  mocks.when(mockAcctsSelector.SObjectType)).thenReturn(Account.SObjectType);
  mocks.when(mockAcctsSelector
      .selectWithOpposById((set<ID>)fflib_match.anyObject())
      )
      .thenReturn(mockAcctsWithOppos);
  mocks.stopStubbing();

  // Given injected mocks
  Application.Selector.setMock(mockAcctsSelector);

  // When doSomeWork is invoked
  new MyClass().doSomeWork(new set<ID> {});  // don't care about real AccountIds

  // Then verify (not shown here; 
  // perhaps verify uow.registerNew or uow.registerDirty)

So, the question, is, how do we create mockAcctsWithOppos since we need to have values for Opportunity.FormulaField__c?

There is only one way and that is to create JSON and deserialize into the Account Sobject. I’ve used three ways to do this:

Mocking SObjects with Json – method 1 – hard-coded strings

Account[] mockAcctsWithOppos = 
   (Account[]) Json.deserialize(someJsonString,list<Account>.class);

where someJsonString looks like this (example is a single Account with two Opportunities):

{
  "totalSize" : 2,
  "done" : true,
  "records" : [ {
    "attributes" : {
      "type" : "Account",
      "url" : "/services/data/v41.0/sobjects/Account/0013600001FGf1HAAT"
    },
    "Id" : "0013600001FGf1HAAT",
    "Opportunities" : {
      "totalSize" : 2,
      "done" : true,
      "records" : [ {
        "attributes" : {
          "type" : "Opportunity",
          "url" : "/services/data/v41.0/sobjects/Opportunity/0063600000PPLTPAA5"
        },
        "Id" : "0063600000PPLTPAA5",
        "FormulaField__c" : "foo"
      },
       {
        "attributes" : {
          "type" : "Opportunity",
          "url" : "/services/data/v41.0/sobjects/Opportunity/0063600000PPLTPAA5"
        },
        "Id" : "0063600000PPLTPAA6",
        "FormulaField__c" : "bar"
      }
    ]
    }
  }
]
}

You create this Json by using Workbench to generate a query and paste the results into either an Apex string or stick in a StaticResource.

Mocking SObjects with Json – method 2 – fflib_ApexMocksUtils.makeRelationship

The ApexMocks package includes a utility method that can construct the deserialized Json without you having to create the actual string

// Let's mock two Accounts, one with two Oppos, the other with none
ID[] mockAcctIds = new list<ID>();
ID[] mockOppoIds = new list<ID>();
for (Integer i = 0; i < 2; i++) {
  mockAcctIds.add(fflib_IdGenerator.generate(Account.SObjectType);
  mockOppoIds.add(fflib_IdGenerator.generate(Opportunity.SObjectType);
}

Account[] mockAcctsWithOppos = fflib_ApexMocksUtils.makeRelationship(
   Account.class,
   new list<Account> {
     new Account(Id = mockAcctIds[0], Name = '00Account'),
     new Account(Id = mockAcctIds[1], Name = '01Account')
   },
   Opportunity.Account,  // the relationship field
   new list<list<Opportunity>> {
     new list<Opportunity> { . // Two Oppos for Account[0]
         new Opportunity(Id = mockOppoIds[0], AccountId = mockAcctIds[0], 
                         FormulaField__c = 'foo'), 
         new Opportunity(Id = mockOppoIds[1], AccountId = mockAcctIds[0], 
                         FormulaField__c = 'bar')
     },
     new list<Opportunity>();  // no Oppos for Account[1]
  );

This is nice as it lets you do everything without messy string constants. But the utility is limited to only one child relationship so you can’t use it for mocking Accounts with Cases and Opportunities. I find the list> 4th argument to initially be confusing to construct and get right.

Mocking SObjects with Json – method 3 – sfab_SObjectFabricator

Matt Addy has a nice GitHub package to construct Sobjects that is more descriptive and isn’t limited by the number of children. Here is how to use it:

// Let's mock two Accounts, one with two Oppos, the other with none
ID[] mockAcctIds = new list<ID>();
ID[] mockOppoIds = new list<ID>();
for (Integer i = 0; i < 2; i++) {
  mockAcctIds.add(fflib_IdGenerator.generate(Account.SObjectType);
  mockOppoIds.add(fflib_IdGenerator.generate(Opportunity.SObjectType);
}
Account[] mockAcctsWithOppos = new list<Account> {
  (Account) new sfab_FabricatedSObject(Account.class) [0] has 2 Oppos
      .setField(Account.Id = mockAcctIds[0]).
      .setField(Account.Name = '00Account')
      .setChildren('Opportunities', new List<sfab_FabricatedSObject> {
        new sfab_FabricatedSObject(Opportunity.class)
          .setField(Opportunity.Id, mockOppoIds[0]),
          .setField(Opportunity.AccountId, mockAcctIds[0]),
          .setField(Opportunity.FormulaField__c, 'foo'), 
        new sfab_FabricatedSObject(Opportunity.class)
          .setField(Opportunity.Id, mockOppoIds[1]),
          .setField(Opportunity.AccountId, mockAcctIds[0]),
          .setField(Opportunity.FormulaField__c, 'bar')
    }).toSObject(),
  (Account) new sfab_FabricatedSObject(Account.class) //[1] has no Oppos
      .setField(Account.Id = mockAcctIds[1])
      .setField(Account.Name = '01Account')
      .toSObject()
  };

I like the sfab_SObjectFabricator approach as it is clear where you are defining children (and you can also do parents with setParent()).

Some final remarks

  1. Don’t let the inability to construct SObjects with formula or audit fields get in your way to using ApexMocks to mock either inputs to services or domain layers or mock results from services or domain layers. Choose one of the approaches above or roll your own to exploit Salesforce’s feature of constructing any Sobject’s fields via Json deserialization.
  2. The examples above are probably too verbose for the code-under-test. The Opportunity.AccountId, if never referenced, need not be mocked.
  3. Exploit the Unit of Work layer so you can use ApexMocks to verify that your DML (via registerXXX methods) is done as expected – without having to pay for the cost of real DML.
  4. You will still need what I call ‘end-to-end’ testing that doesn’t use mocks to verify that your selectors work against real data and return all the columns the code expects. You also need to verify that actual DML doesn’t run afoul of Validation Rules that otherwise aren’t executed when you mock the Unit Of Work.
  5. ApexMocks are a great way to explore in detail the unit test use cases by focusing the testing problem on the inputs and outputs of a given class/method.

Leave a Reply

Your email address will not be published. Required fields are marked *