apex-enterprise-patterns / fflib-apex-common

Common Apex Library supporting Apex Enterprise Patterns and much more!
BSD 3-Clause "New" or "Revised" License
899 stars 514 forks source link

Selector Mocks to Include SOQL Query Retrieve Check #454

Closed seanong-athena closed 1 year ago

seanong-athena commented 1 year ago

I implemented a selector class:

public inherited sharing class AccountsSelector extends fflib_SObjectSelector implements IAccountsSelector {

  public List<Schema.SObjectField> getSObjectFieldList() {
    return new List<Schema.SObjectField> {
      Account.Id,
      Account.Name
    }
  }

  public List<Account> selectAllAccounts() {
    String query = newQueryFactory().toSOQL();
    return (List<Account>)Database.query(query)
  }
}

I then used the selector in a method, which will fail as expected with the error SObject row was retrieved via SOQL without querying the requested field

public static void testMethod() {
  List<Account> accts = AccountsSelector.newInstance().selectAllAccounts();
  String value = accts[0].Unqueried_Field__c;
}

In a test class, I write the test using the Salesforce standard way (using DML) and it fails as expected:

@isTest
static void testingMethod() {
  Account acct = new Account(Name='Test');
  insert acct;
  TestService.testMethod();
}

However, if I write the test using apex mocks, the test will run successfully when it is hoped that it will fail:

@isTest
static void testingMethodUsingApexMocks() {
  Account acct = new Account();
  fflib_ApexMocks mocks = new fflib_ApexMocks();
  IAccountsSelector sel = (IAccountsSelector)mocks.mock(IAccountsSelector.class);
  mocks.startStubbing();
  mocks.when(sel.sObjectType()).thenReturn(Account.SObjectType);
  mocks.when(sel.selectAllAccounts()).thenReturn(new List<Account>{acct});
  mocks.stopStubbing();
  TestApp.Selector.setMock(sel);
  TestService.testMethod();
}

Feature request: When mocking selectors, the output will only contain the fields retrieved by the query. I know that there is a method SObject.getPopulatedFieldsAsMap() and I wonder if this could be incorporated somehow.

daveespo commented 1 year ago

The purpose of using mocks is to avoid interaction with the database.

Unfortunately, the only way to generate the precise 'SObject row was retrieved via SOQL without querying the requested field' when accessing an SObject is to have retrieved it from the database. Pure unit tests are not a substitute/replacement for some amount of integration test coverage -- you should be testing for the above message in one of your integration tests.

We have no plans to emulate that logic in the mocking framework as it would be nearly impossible to do.