Skip to content

Instantly share code, notes, and snippets.

@gdoenlen
Last active October 19, 2023 11:42
Show Gist options
  • Save gdoenlen/f539f45f5055c90a3dc8ff3669a0a792 to your computer and use it in GitHub Desktop.
Save gdoenlen/f539f45f5055c90a3dc8ff3669a0a792 to your computer and use it in GitHub Desktop.
Simple dependency injection within SFDC

This is a simple implementation of dependency injection within SFDC. The main goal is to abstract away any inlined instructions that are native to apex or that are implemented as static methods instead of instance. The main benefit of abstracting these things away is that it will allow you to mock them when testing. The main offenders of things being inlined are DML actions like insert, update, or delete or any calls to the System.Database class and SOQL/SOSL queries.

Repository class: This class is designed to to do any crud actions or anything that you would do via the System.Database class. Its design is simple in that it just forwards to the inlined crud/database call you would want to use. You can take this class a step further and catch all exceptions, log them, and rethrow or provide common functionality like making sure the arguments aren't null or empty.

Selector classes: These classes provide consistent queries to access your database. You would have one per entity type. Abstracting these provides several benefits: it allows them to be more thoroughly tested, reusable, and can be mocked when testing classes that depend on them. Andy Fawcett has good blogs about the selector layer and goes further in depth about the benefits.

Dependency injection: Dependency injection can occur in 3 areas: Triggers, Visualforce controllers, and Aura controllers. I've provided a simple pattern of constructor based dependency injection that passes these dependencies down to the classes that need them. Dependencies provide a public INSTANCE variable that makes creating and using them easier.

In triggers, they are passed directly to whatever abstraction you are using to handle your trigger invocations.

In visualforce pages SFDC needs an empty constructor to create the controller so you should provide a second constructor that takes the dependencies the controller needs. You would then call this constructor with your dependency instances.

In aura controllers you auraenabled methods must be static, this requirement forces us to forward any functionality to an INSTANCE of our self.

Bringing it all together: So what is the point of doing all this work? Mocking! After we've implemented these patterns we can use a mocking library like financial force's apex mocks to greatly speed up our development time and deployment time!

Caveats and where this pattern fails: This pattern will fail once your codebase begins to grow. The main problem areas I see are within the selector classes as the amount of queries you have grows and the different number of fields we want to return changes. In this situtation I would then implement something like financial force's Selector layer from its apex commons library.

It will also fail as your amount of required fields and validation rules change throughout the life of your application. This is because your validation rules/required fields will never actually be fired as all IO is being mocked away and the database never actually contacted. In other languages this is solved by validation happening before any of your business logic begins to execute. You could implement this by providing a validator that fires before insert and validates that all required fields are present etc. This will add some development time, but I think the time saved writing unit tests and avoiding 5 hour long deployments is worth it. Another solution is to write smaller integration tests for specific functionality you need so you can be sure it doesn't break later. (i.e. if you are inserting opportunities with null accountId's write a test that does that, so you can be sure it will succeed even if you introduce validations later) This will still be leagues faster as you are only inserting 1 record 1 time instead of hundreds over the course of your tests and triggers.

The last caveat is that you will have certain parts of code that isn't covered, mainly the injection sites. It is important that the injection sites are simple, without complex logic and should mainly only contain construction as these are validated at compile time.

/**
* Selector for the `Account` entity
*/
public class AccountSelector {
public static final AccountSelector INSTANCE = new AccountSelector();
/**
* Finds all accounts that are child accounts of
* the given opportunity's account.
*
* @param oppIds the Id's of the opportunitys you want to filter by
* @return all child accounts of accounts of the opps given
*/
public List<Account> findByParentIdInOpportunityAccountId(Set<Id> oppIds) {
return [
SELECT Id, Name, RecordType.Name, Status__c, ParentId
FROM Account
WHERE ParentId
IN (SELECT AccountId FROM Opportunity WHERE Id IN :oppIds)
AND ParentId != null
];
}
}
public class LexComponentControllerExample {
public static final LexComponentControllerExample INSTANCE
= new LexComponentControllerExample(Repository.INSTANCE);
private final Repository repo;
public LexComponentControllerExample(Repository repo) {
Objects.assertNotNull(repo);
this.repo = repo;
}
private void privateSave(Account a) {
try {
repo.upd(a);
} catch (DmlException ex) {
throw new AuraHandledException(ex.getMessage());
}
}
@AuraEnabled
public static void save(Account a) {
INSTANCE.privateSave(a);
}
}
/**
* Trigger for the `Opportunity` entity
*/
trigger OpportunityTrigger on Opportunity (before insert, before update, after insert, after update) {
Repository repo = Repository.INSTANCE;
AccountSelector sel = AccountSelector.INSTANCE;
//you inject your dependencies into whatever abstraction you use to handle your triggers.
OpportunityTriggerManager mgr = new OpportunityTriggerManager(repo, sel);
mgr.execute();
}
//make believe class to handle trigger executions
public class OpportunityTriggerManager extends TriggerManager {
private final Repository repo;
private final AccountSelector acctSel;
public OpportunityTriggerManager(Repository repo, AccountSelector acctSel) {
Objects.assertNotNull(repo);
Objects.assertNotNull(sel);
this.repo = repo;
this.acctSel = acctSel;
}
public override void execute() {
//do stuff
}
}
/**
* Repository class for CRUD operations
*/
public virtual class Repository {
/**
* Singleton repository instance.
* This instance should be used whenever injecting the repository.
*/
public static final Repository INSTANCE = new Repository();
/**
* Inserts the sobjects into the database.
* Uses a simple `insert` call.
*
* @param sobjs the sobjects you want to insert
* @return the sobjects after insertion
* @throws DmlException
*/
public virtual List<SObject> ins(List<SObject> sobjs) {
if (!sobjs.isEmpty()) { insert sobjs; }
return sobjs;
}
/**
* Helper for inserting just 1 record
*
* @param sobj the record to insert
* @return the record after inserting
* @throws DmlException
*/
public virtual SObject ins(SObject sobj) {
return ins(new List<SObject> { sobj }).get(0);
}
/**
* Updates the sobjects into the database
* Uses a simple `update` call.
*
* @param sobjs the sobjects you want to update
* @return the sobjects after being updated
* @throws DmlException
*/
public virtual List<SObject> upd(List<SObject> sobjs) {
if (!sobjs.isEmpty()) { update sobjs; }
return sobjs;
}
/**
* Helper for updating a single record
*
* @param sobj the sobject to update
* @return the sobj after updating
* @throws DmlException
*/
public virtual SObject upd(SObject sobj) {
return upd(new List<SObject>{ sobj }).get(0);
}
/**
* Deletes the given sobjs from the database. Uses a simple `delete` call
*
* @param sobjs the sobjects you want to delete
* @return the sobjs after deletion
* @throws DmlException
*/
public virtual List<SObject> del(List<SObject> sobjs) {
if (!sobjs.isEmpty()) { delete sobjs; }
return sobjs;
}
/**
* Helper for deleting 1 sobject
*
* @param sobj the sobject to delete
* @return the sobject after deleting
* @throws DmlException
*/
public virtual SObject del(SObject sobj) {
return del(new List<SObject>{ sobj }).get(0);
}
/**
* Undeletes the given sobjs from the database. Uses a simple `undelete` call
*
* @param sobjs the sobjs you want to undelete
* @return the sobjects after undeletion
* @throws DmlException
*/
public virtual List<SObject> undel(List<SObject> sobjs) {
if (!sobjs.isEmpty()) { undelete sobjs; }
return sobjs;
}
/**
* Helper for undeleting a single record
*
* @param sobj the sobject to undelete
* @return the sobj after undeleting
* @throws DmlException
*/
public virtual SObject undel(SObject sobj) {
return undel(new List<SObject>{ sobj }).get(0);
}
/**
* Converts a lead using the standard Database.LeadConvert function
*
* @param leadConverts the leads you want to convert
* @return the result of the lead convert
* @throws DmlException
* @see Database.convertLead
*/
public virtual List<Database.LeadConvertResult> convertLead(List<Database.LeadConvert> leadConverts) {
return Database.convertLead(leadConverts);
}
/**
* Helper function for converting a single lead
*
* @param leadConvert the lead you want to convert
* @return the result of the conversion
* @throws DmlException
*/
public virtual Database.LeadConvertResult convertLead(Database.LeadConvert leadConvert) {
return convertLead(new List<Database.LeadConvert>{ leadConvert }).get(0);
}
}
public class VfControllerExample {
private final Repository repo;
//needed for SFDC to create the page
public VfControllerExample() {
this(Repository.INSTANCE)
}
//needed for mocking
public VfControllerExample(Repository repo) {
Objects.assertNotNull(repo);
this.repo = repo;
}
}
@justin-lyon
Copy link

I really need to wrap my head around ApexMocks. I have no idea what Apex feature allows ApexMocks to do what it does. .when, .thenReturn, .verify. That all looks so alien to anything I've seen in Apex before. It looks more like Javascript than Apex.

// When ctxSel.someMethod, cast an fflib_Match of StringSetMatcher to a set of String? Then return an empty list of Context_Log__c?
mocks.when(ctxSel.someMethod((Set<String>) fflib_Match.matches(new StringSetMatcher()))

@gdoenlen
Copy link
Author

@jlyon87 Yes basically its saying when this method is called with X parameter then return Y value. ApexMocks is based around https://github.com/mockito/mockito so maybe looking at tutorials or docs for that could help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment