Using Separation of Concerns to enable Mocking – part 1

In the Salesforce world, I’ve often had discussions with developers about what unit tests in Salesforce should include? Should we include tests for workflows, processes, flows, validation rules? I’ve always been of the opinion that unit tests should be solitary rather than sociable (see this page on MartinFowler.com for more information) and therefore we shouldn’t be dependent on creating or updating data (and the associated declarative automation that comes with that). To help us enable more solitary unit tests, we’ve recently been looking at the Apex Stub Provider Interface and Apex Mocks from the Financial Force Library and have started to use the following simple pattern to help us; but we’ve only been able to get to this point by adopting separation of concerns.

Service, Selector and Domain Layers

For a long time now, we’ve followed some of the practices from the Separation of Concerns blogs from Andy In The Cloud and Trailhead modules as standard, and depending on the size of project our default starting place is the service layer. Writing unit tests for service methods has served us well; however, we haven’t always used selector or domain layers and thus unit testing remains largely sociable (we use the test setup to insert the records we need to query against). This is a fundamental issue with moving to mocking. Whether you use the Financial Force Library to help with your selector or domain layers or not, creating those layers will help separate out the SOQL and DML from your service layer which ultimately will allow you to write mocks.

Lets look at the following simple example…

public class AccountService {
     
     public static List<Account> findOrCreateBillingAccount(Set<String> billingAccountNames) {
          Id recordTypeId = RecordTypeUtil.instance(Account.SObjectType).getRecordTypeIdByName('Billing Account');
          List<Account> accounts = [select Id, Name from Account where RecordTypeId = :recordTypeId and Name in :billingAccountNames];
          List<Account> accountsToCreate = new List<Account>();
          Set<String> foundNames = ListUtils.getNames(accounts);
          for (String billingAccountName : billingAccountNames) {
               if (!foundNames.contains(billingAccountName)) {
                    accountsToCreate.add(createBillingAccount(billingAccountName));
               } 
          }
          if (!accountsToCreate.isEmpty()) {
               Database.insert(accountsToCreate);
               accounts.addAll(accountsToCreate);
          )
          return accounts;
     }

     private Account createBillingAccount(string name) {
          // Set default properties on account
          Id recordTypeId = RecordTypeUtil.instance(Account.SObjectType).getRecordTypeIdByName('Billing Account');
          // TODO: set additional properties here...

          return new Account(Name = name, RecordTypeId = recordTypeId);
     }
}

While the above is an overly simplified example, it shows where the problem with unit tests can arise; we may have adopted some degree of separation and we can call the service method from a trigger, from an API, from a Lightning Component or from a VisualForce page which is a good start but our SOQL and DML is tightly coupled into the service method so we are currently unable to mock this, meaning we have to setup data for our unit tests. While this might not seem much of an issue, if we were working with an opportunity service which was dependent on product setup and creation of accounts and contacts then it starts to get more of an issue.

Lets look at what happens if we start to add in a selector layer…

public class AccountSelector {
     public List<Account> selectById(Set<Id> idSet) {
          return [select Id, Name from Account where Id in :idSet];
     }

     public List<Account> selectBillingAccountsByName(Set<String> nameSet) {
          Id recordTypeId = RecordTypeUtil.instance(Account.SObjectType).getRecordTypeIdByName('Billing Account');

          return [select Id, Name from Account where RecordTypeId = :recordTypeId and Name in :nameSet];
     }
}

public class AccountService {
     @TestVisible
     private static AccountSelector selector = new AccountSelector();
     
     public static List<Account> findOrCreateBillingAccount(Set<String> billingAccountNames) {
          List<Account> accounts = selector.selectBillingAccountsByName(billingAccountNames);
          List<Account> accountsToCreate = new List<Account>();
          Set<String> foundNames = ListUtils.getNames(accounts);
          for (String billingAccountName : billingAccountNames) {
               if (!foundNames.contains(billingAccountName)) {
                    accountsToCreate.add(createBillingAccount(billingAccountName));
               } 
          }
          if (!accountsToCreate.isEmpty()) {
               Database.insert(accountsToCreate);
               accounts.addAll(accountsToCreate);
          )
          return accounts;
     }

     private Account createBillingAccount(string name) {
          // Set default properties on account
          Id recordTypeId = RecordTypeUtil.instance(Account.SObjectType).getRecordTypeIdByName('Billing Account');
          // TODO: set additional properties here...

          return new Account(Name = name, RecordTypeId = recordTypeId);
     }
}

In the above code, we have separated out the SOQL into its own selector class; this is a great next move on the path to enabling mocks in our tests. We can now mock the selector, and write tests that cover scenarios specifically to the logic in your service class. If we want to get 100% coverage and test all scenarios, then we also need to remove the DML. Again, consider this was an opportunity service class and we were trying to insert an opportunity but we had mocked the account – the insert would fail.

Below we’ll split out the DML into a domain class. Now when we come to write our unit tests we can mock both the selector and the domain, achieve 100% test coverage, cover all logic implemented in the service but without the need to insert any test data.

public class AccountSelector {
     public List<Account> selectById(Set<Id> idSet) {
          return [select Id, Name from Account where Id in :idSet];
     }

     public List<Account> selectBillingAccountsByName(Set<String> nameSet) {
          Id recordTypeId = RecordTypeUtil.instance(Account.SObjectType).getRecordTypeIdByName('Billing Account');

          return [select Id, Name from Account where RecordTypeId = :recordTypeId and Name in :nameSet];
     }
}

public class AccountDomain {
     public List<Account> insertAccounts(List<Account> accounts) {
          Database.insert(accounts);
          return accounts;
     }

     public List<Account> updateAccounts(List<Account> accounts) {
          Database.update(accounts);
          return accounts;
     }
 
     public Account createBillingAccount(string name) {
          // Set default properties on account
          Id recordTypeId = RecordTypeUtil.instance(Account.SObjectType).getRecordTypeIdByName('Billing Account');
          // TODO: set additional properties here...

          return new Account(Name = name, RecordTypeId = recordTypeId);
     }
}

public class AccountService {
     @TestVisible
     private static AccountSelector selector = new AccountSelector();
     @TestVisible
     private static AccountDomain domain = new AccountDomain();
     
     public List<Account> findOrCreateBillingAccount(Set<String> billingAccountNames) {
          List<Account> accounts = selector.selectBillingAccountsByName(billingAccountNames);
          List<Account> accountsToCreate = new List<Account>();
          Set<String> foundNames = ListUtils.getNames(accounts);
          for (String billingAccountName : billingAccountNames) {
               if (!foundNames.contains(billingAccountName)) {
                    accountsToCreate.add(domain.createBillingAccount(billingAccountName));
               } 
          }
          if (!accountsToCreate.isEmpty()) {
               domain.insertAccounts(accountsToCreate);
               accounts.addAll(accountsToCreate);
          )
          return accounts;
     }
}

In part 2 we’ll look at the some unit tests and how the mocks can help us…

Leave a Comment

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