The Repository Pattern
./img/joys-of-apex-thumbnail.png
Welcome back to The Joys Of Apex. We've covered some fun ground with Mocking DML, but now it's time to take your use of mocks in Apex to the next level. The end goal is to provide you with options when it comes to creating a system structure that allows you to easily get data where you need it and update that data easily in your tests. You can opt-in to this strategy if it works for you and makes sense.
I say that precisely because one of the reasons that Apex is so great is that the typical hoops you have to jump through in order to interact with a database within an object-oriented programming language have been abstracted away for you, and the existing SOQL (Salesforce Object Query Language) implementation allows for some really powerful things within your codebase. Typically, SOQL usage looks something like this:
1linkList<Account> accounts = [SELECT Id, Name FROM Account WHERE Name = 'Acme'];
As somebody who also does quite of bit of .net programming, the sheer simplicity of interacting with the database within Apex is refreshing, to say the least. The fact that you can escape out from Apex to inject variables like lists of Salesforce Ids or lists of strings that serve as further filtering criteria is incredible. I'm advising you to not use these features. That's pretty ... polarizing ... within the SFDC community, and I can understand if you don't want to follow me down this road.
There are two problems with utilizing SOQL like the above example:
But how do we break out from this situation? How do we work towards a place where the tests can easily replicate the expected system data without having to essentially code for two different purposes — one for testing, and one for the production code? As an example of a method that isn't scalable, here's one possible approach:
1link//using the Selector pattern ...
2linkpublic virtual class OpportunityLineItemRepo {
3link public virtual List<OpportunityLineItem> getLineItems(Set<Id> oppIds) {
4link return [
5link SELECT Id, Description
6link FROM OpportunityLineItem
7link WHERE OpportunityId = :oppIds
8link ];
9link }
10link}
11link
12link//the usage
13linkpublic class OpportunityUpdater {
14link private final OpportunityLineItemRepo oppLineItemRepo;
15link
16link public OpportunityUpdater(OpportunityLineItemRepo oppLineItemRepo) {
17link this.oppLineItemRepo = oppLineItemRepo;
18link }
19link}
20link
21link//and then in your tests...
22link@isTest
23linkprivate class OpportunityUpdater_Tests {
24link @isTest
25link static void it_should_update_opportunities_correctly() {
26link //assuming we have all of these objects already initialized
27link
28link //arrange
29link List<Opportunity> opps = [SELECT Id, Description FROM Opportunity LIMIT 2];
30link Opportunity firstOpp = opps[0];
31link Opportunity secondOpp = opps[1];
32link
33link List<OpportunityLineItem> lineItems = new List<OpportunityLineItem>{
34link new OpportunityLineItem(
35link OpportunityId = firstOpp.Id,
36link Description = 'business logic criteria'
37link )
38link };
39link
40link //act
41link OppLineItemRepoMock mock = new OppLineItemRepoMock();
42link mock.LineItems = lineItems;
43link new OpportunityUpdater(mock).updateOppsOnClose(opps);
44link
45link //assert
46link System.assertEquals('Magic Business String', firstOpp.Description);
47link System.assertNotEquals('Magic Business String', secondOpp.Description);
48link }
49link
50link
51link private class OppLineItemRepoMock extends OpportunityLineItemRepo {
52link public List<OpportunityLineItem> LineItems { get; set; }
53link
54link public override List<OpportunityLineItem> getLineItems(Set<Id> oppIds) {
55link return LineItems;
56link }
57link }
58link}
OK, yikes. That was a lot of code just to prove a point — namely that going this route (which builds to the Selector pattern, where all your queries are encapsulated by methods that can then be overridden) is unsustainable. You'll need to mock every method that leads to a SOQL query; you'll need many different methods to add different filtering criteria. The Selector pattern requires a different method for each query you require, and if you'd like to override your selector methods, you're going to have your work cut out for you.
Correctly implementing the Repository pattern means that you only need one seam, or spot where your tests do something differently from your production level code. But let's do it in true TDD style — starting with the tests. We can even start from the same code we already have. Let's assume that over time, in a completely different section of the codebase, we're faced with a request to fetch OpportunityLineItems in order to verify that the correct Order has been created for a customer on a daily basis. If the Order needs to be updated, we want to update the existing Opportunity as well. Management will use the Description field on the Opportunity to filter on (for the purposes of this example, trying to stick to the simplest included fields possible), as they want to keep track of how often the Sales team is incorrectly keying things. This is going to lead to a potential regression in the existing code, as well as balloon the production code to deal with both scenarios. That won't be initially obvious to the team making these changes, though ...
I normally don't operate this far "down' the Salesforce sales pipeline when doing examples, because most of the "top" part of the funnel is shared between almost all SFDC orgs; whether you're using Person Accounts (sorry), or classic B2B, odds are strong that you use Opportunities, Leads, Accounts, and Contacts (and for Person Accounts, you can imagine that the Contact examples are just the corresponding fields on Person Accounts). For this example, though, I want to show that as a business expands, its business logic oftentimes leads to existing Salesforce objects being accessed in completely different ways. In order to prevent linear code growth — and the corresponding increase in complexity and understanding that comes with that — we want to be able to recognize commonalities shared by differing business needs.
1linkpublic class OrderUpdater {
2link private final OpportunityLineItemRepo oppLineItemRepo;
3link
4link public OrderUpdater(OpportuniyLineItemRepo oppLineItemRepo) {
5link this.oppLineItemRepo = oppLineItemRepo;
6link }
7link
8link public void checkOrders(List<Order> orders) {
9link Map<Id, List<Order>> accountIdToOrder = new Map<Id, List<Order>>();
10link for(Order order : orders) {
11link if(accountIdToOrder.containsKey(order.AccountId)) {
12link List<Order> accountOrders = accountIdToOrder.get(order.AccountId);
13link accountOrders.add(order);
14link } else {
15link accountIdToOrder.put(order.AccountId, new List<Order>{ order });
16link }
17link }
18link //for now we use the raw SOQL
19link //it's the TDD way!
20link List<Opportunity> associatedOpps = [
21link SELECT Id, Description
22link FROM Opportunity
23link WHERE IsWon = true
24link AND AccountId = :accountIdToOrder.keySet()
25link ];
26link
27link Map<Id, Opportunity> oppIdToOpp = new Map<Id, Opportunity>(associatedOpps);
28link
29link List<OpportunityLineItem> lineItems = this.oppLineItemRepo.getLineItems(oppIdToOpp.keySet());
30link for(OpportunityLineItem lineItem : lineItems) {
31link if(lineItem.Description == 'order related business logic criteria') {
32link Opportunity opp = oppIdToOpp.get(lineItem.OpportunityId);
33link opp.Description = 'Order Error';
34link }
35link }
36link //etc, imagine we update the corresponding order
37link //now that we know something's wrong ...
38link }
39link}
40link
41link//and in your test class ...
42link@isTest
43linkprivate class OrderUpdater_Tests {
44link @isTest
45link static void it_should_identify_correct_orders_based_on_opportunity_line_items() {
46link //assuming things are already setup
47link //arrange
48link Order order = [SELECT Id, AccountId FROM Order LIMIT 1];
49link Order.Description = 'Original';
50link
51link List<OpportunityLineItem> lineItems = new List<OpportunityLineItem>{
52link new OpportunityLineItem(
53link OpportunityId = firstOpp.Id,
54link Description = 'order related business logic criteria'
55link )
56link };
57link
58link //act
59link OppLineItemRepoMock mock = new OppLineItemRepoMock();
60link mock.LineItems = lineItems;
61link new OrderUpdater(mock).checkOrders(new List<Order>{ order });
62link
63link //assert
64link System.assertEquals('Our new status', order.Description);
65link }
66link}
67link
68link//uh oh! We need the mock again
69link//for now let's pretend
70link//we moved it to its own class
71linkpublic class OppLineItemRepoMock extends OpportunityLineItemRepo {
72link public List<OpportunityLineItem> LineItems { get; set; }
73link
74link public override List<OpportunityLineItem> getLineItems(Set<Id> oppIds) {
75link return LineItems;
76link }
77link}
Another complicated example, and very contrived. But I hope it helps to show a few things:
checkOrders
, but in the end, the cleanup is only going to add lines of code. As Salesforce developers, we iterate through lists, sets, and maps like crazy. Reducing the number of times we need to do that is going to help; making our database-fetching methods more controllable reduces some of the in-line iteration we need to perform to compare our objects.OrderUpdater
is operating off of the same Description field as the OpportunityUpdater
, the value for Description might get out of sync depending on which object fetches the Opportunities first.1link@isTest
2linkpublic class MockFactory {
3link
4link public static OppLineItemRepoMock getLineItemMock() {
5link return new OppLineItemRepoMock();
6link }
7link
8link private class OppLineItemRepoMock extends OpportunityLineItemRepo {
9link //inners
10link }
11link}
But that's only going to help if every test is calling the mock in the same way. If you needed to verify that a query was being made in a particular way on top of the fact that your expected line items were being returned, you're in for a world of refactoring hurt.
We're about to Kent Beck this whole thing. Indeed, this whole example was inspired by Kent Beck's famous "Money" example from Test Driven Development By Example. Let's start by creating a way to compose SOQL queries. We'd like for our repository to eventually be able to replicate the best of SOQL while remaining strongly typed; setting it up in such a way that no matter how many repositories are in use, there's only one per SObject and we only need to flip one switch in our tests in order to gain access to it. Here's a test showing my ideal syntax:
1link@isTest
2linkprivate class Repository_Tests {
3link @isTest
4link static void it_should_take_in_a_query() {
5link Query basicQuery = new Query(Opportunity.IsWon, Query.Operator.EQUALS, true);
6link IRepository repo = new Repository(
7link Opportunity.SObjectType,
8link new List<SObjectField>{
9link Opportunity.Id
10link }
11link );
12link
13link repo.get(basicQuery);
14link System.assertEquals(1, Limits.getQueries());
15link }
16link}
As is often the case with TDD, starting with a big problem (in even getting the code to compile), means you have to stub out quite a bit just to get the code to compile.
First we'll need a way to represent queries ... we want our Query object to be comparable to another query, to consume SObjectFields, and to properly represent a few special cases in SOQL queries:
1link@isTest
2linkprivate class Query_Tests {
3link @isTest
4link static void it_should_encapsulate_sobject_fields_and_values() {
5link Query basicQuery = new Query(Opportunity.IsWon, Query.Operator.EQUALS, true);
6link
7link System.assertEquals('IsWon = true', basicQuery.toString());
8link }
9link
10link @isTest
11link static void it_should_equal_another_query_with_the_same_values() {
12link Query basicQuery = new Query(Opportunity.IsWon, Query.Operator.EQUALS, true);
13link Query sameQuery = new Query(Opportunity.IsWon, Query.Operator.EQUALS, true);
14link System.assertEquals(basicQuery, sameQuery);
15link }
16link
17link @isTest
18link static void it_should_properly_render_datetimes_as_strings() {
19link Datetime sevenDaysAgo = System.now().addDays(-7);
20link Query basicQuery = new Query(
21link Opportunity.CreatedDate,
22link Query.Operator.GREATER_THAN_OR_EQUAL,
23link sevenDaysAgo
24link );
25link
26link System.assertEquals(
27link 'CreatedDate >= ' +
28link sevenDaysAgo.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time'),
29link basicQuery.toString()
30link );
31link }
32link}
33link
34link//and then for the Repository ...
35link@isTest private class Repository_Tests {
36link @isTest
37link static void it_should_take_in_a_query() {
38link Query basicQuery = new Query(Opportunity.IsWon, Query.Operator.EQUALS, true);
39link IRepository repo = new Repository(Opportunity.SObjectType, new List<SObjectField>{
40link Opportunity.Id
41link });
42link
43link repo.get(basicQuery);
44link System.assertEquals(1, Limits.getQueries());
45link }
46link
47link @isTest
48link static void it_should_handle_lists_and_sets_of_ids_or_strings() {
49link Id accountId = TestingUtils.generateId(Account.SObjectType);
50link List<Id> ids = new List<Id>{ accountId, accountId };
51link Set<Id> setIds = new Set<Id>(ids);
52link Set<String> oppNames = new Set<String>{ 'Open', 'Closed' };
53link
54link Query listQuery = new Query(Opportunity.Id, Query.Operator.EQUALS, ids);
55link Query setQuery = new Query(Opportunity.Id, Query.Operator.EQUALS, setIds);
56link Query setStringQuery = new Query(Opportunity.Name, Query.Operator.EQUALS, oppNames);
57link
58link IRepository repo = new Repository(Opportunity.SObjectType, new List<SObjectField>{
59link Opportunity.Id
60link });
61link
62link repo.get(listQuery);
63link repo.get(setQuery);
64link repo.get(setStringQuery);
65link System.assertEquals(3, Limits.getQueries());
66link //we need to write a special assert for sets with multiple values
67link System.assertEquals('Name in (\'Closed\',\' Open\')', setStringQuery.toString());
68link }
69link}
This is already going to be a long post. Kent Beck wrote "Test Driven Development By Example" in book format for a reason ... I'd love to hand-write out each iteration of the Query and Repository classes so that you can see how they develop, but we'll have to save that exercise for another time, and you'll be able to see the full code online at the end. I'll cut to the chase and show the rudimentary implementations:
1linkpublic class Query {
2link public enum Operator {
3link EQUALS,
4link NOT_EQUALS,
5link LESS_THAN,
6link LESS_THAN_OR_EQUAL,
7link GREATER_THAN,
8link GREATER_THAN_OR_EQUAL
9link }
10link
11link private final SObjectField field;
12link private final Operator operator;
13link private final List<Object> predicates;
14link
15link private static Boolean isSet = false;
16link
17link public Query(SObjectField field, Operator operator, Object predicate) {
18link this(field, operator, new List<Object>{ predicate });
19link }
20link
21link public Query(SObjectField field, Operator operator, List<Object> predicates) {
22link this.field = field;
23link this.operator = operator;
24link this.predicates = predicates;
25link }
26link
27link public override String toString() {
28link String fieldName = this.field.getDescribe().getName();
29link String predName = this.getPredicate(this.predicates);
30link return fieldName + ' ' + this.getOperator() + ' ' + predName;
31link }
32link
33link public Boolean equals(Object thatObject) {
34link if(thatObject instanceof Query) {
35link Query that = (Query) thatObject;
36link return this.toString() == that.toString();
37link }
38link
39link return false;
40link }
41link
42link private String getOperator() {
43link Boolean isList = this.predicates.size() > 1;
44link switch on this.operator {
45link when EQUALS {
46link return isList || isSet ? 'in' : '=';
47link }
48link when NOT_EQUALS {
49link return isList || isSet ? 'not in' : '!=';
50link }
51link when LESS_THAN {
52link return '<';
53link }
54link when LESS_THAN_OR_EQUAL {
55link return '<=';
56link }
57link when GREATER_THAN {
58link return '>';
59link }
60link when GREATER_THAN_OR_EQUAL {
61link return '>=';
62link }
63link when else {
64link return null;
65link }
66link }
67link }
68link
69link private String getPredicate(Object predicate) {
70link if(predicate == null) {
71link return 'null';
72link } else if(predicate instanceof Datetime) {
73link //the most annoying one
74link Datetime dt = (Datetime) predicate;
75link return dt.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time');
76link } else if(predicate instanceof List<Object>) {
77link List<Object> predicates = (List<Object>) predicate;
78link List<String> innerStrings = new List<String>();
79link for(Object innerPred : predicates) {
80link //recurse for string value
81link String innerString = this.getPredicate(innerPred);
82link innerStrings.add(innerString);
83link }
84link String start = innerStrings.size() > 1 ? '(' : '';
85link String ending = innerStrings.size() > 1 ? ')' : '';
86link return start + String.join(innerStrings, ',') + ending;
87link } else if(predicate instanceof String) {
88link String input = (String) predicate;
89link return '\'' + String.escapeSingleQuotes(input) + '\'';
90link }
91link
92link String predValue = String.valueOf(predicate);
93link //fun fact - you can detect a list
94link //but you can't detect a set!
95link if(predValue.startsWith('{') && predValue.endsWith('}')) {
96link List<String> setInner = predValue.substring(1, predValue.length() -1).split(',');
97link isSet = setInner.size() > 1;
98link return this.getPredicate(setInner);
99link }
100link return predValue;
101link }
102link}
Our Query class will be consumed by the repository and will offer a type-safe method for SOQL query composition before passing a request to the repository. Some of its methods may grow; for example, my own version of getPredicate
identifies objects like a DateLiteral
, which I've created to encapsulate SOQL queries using values like:
1linkList<Opportunity> opps = [SELECT Id FROM Opportunity WHERE CreatedDate >= TODAY];
to something like:
1linkList<Opportunity> opps = (List<Opportunity>)repo.get(new Query(
2link Opportunity.CreatedDate,
3link Query.Operator.GREATER_THAN_OR_EQUAL,
4link DateLiteral.TODAY)
5link);
I've also collaborated with others on improvements like support for parent-child and child-parent queries. The sky's the limit, really.
You might find such extensions impractical in your own experience; that being said, you should also be able to see how easy it would be to add features like the use of an optional "OR" flag. Similarly, you're about to see a very basic repository; at the same time, it should also be easy to see how you could short-circuit the query if an empty list is passed in, for example (which you'll be able to see on Github).
The repository will take in a query, or a list of queries, and will execute them by composing the rest of the query. This part is pretty simple:
1linkpublic interface IRepository {
2link List<SObject> get(Query query);
3link List<SObject> get(List<Query> queries);
4link}
5link
6linkpublic class Repository implements IRepository {
7link private final Schema.SObjectType repoType;
8link private final List<Schema.SObjectField> queryFields;
9link
10link public Repository(Schema.SObjectType repoType, List<Schema.SObjectField> queryFields) {
11link this.repoType = repoType;
12link this.queryFields = queryFields;
13link }
14link
15link public List<SObject> get(Query query) {
16link return this.get(new List<Query>{ query });
17link }
18link
19link public List<SObject> get(List<Query> queries) {
20link String selectClause = 'SELECT ' + this.addSelectFields();
21link String fromClause = '\nFROM ' + this.repoType;
22link String whereClause = this.addWheres(queries);
23link
24link String finalQuery = selectClause + fromClause + whereClause;
25link System.debug('Query: \n' + finalQuery);
26link List<SObject> results = Database.query(finalQuery);
27link System.debug('Results: \n' + results);
28link return results;
29link }
30link
31link private String addSelectFields() {
32link Set<String> fieldStrings = new Set<String>{ 'Id' };
33link for(SObjectField field : this.queryFields) {
34link fieldStrings.add(field.getDescribe().getName());
35link }
36link return String.join(new List<String>(fieldStrings), ', ');
37link }
38link
39link private String addWheres(List<Query> queries) {
40link List<String> wheres = new List<String>();
41link for(Query query : queries) {
42link wheres.add(query.toString());
43link }
44link return '\nWHERE ' + String.join(wheres, '\nAND');
45link }
46link}
In order to not clutter up the Factory class, I like for the Factory to expose the repositories through a singleton repository factory:
1linkpublic virtual class Factory {
2link public ICrud Crud { get; private set; }
3link public RepoFactory RepoFactory { get; private set;}
4link
5link private static Factory factory;
6link
7link @testVisible
8link protected Factory() {
9link this.Crud = new Crud();
10link this.RepoFactory = new RepoFactory();
11link }
12link
13link public static Factory getFactory() {
14link //production code can only initialize the factory through this method
15link if(factory == null) {
16link factory = new Factory();
17link }
18link
19link return factory;
20link }
21link
22link //factory methods for initializing objects
23link @testVisible
24link private Factory withMocks {
25link get {
26link this.Crud = new CrudMock();
27link this.RepoFactory = new RepoFactoryMock();
28link return this;
29link }
30link }
31link}
32link
33linkpublic virtual class RepoFactory {
34link public virtual IRepository getOppRepo() {
35link List<SObjectField> queryFields = new List<SObjectField>{
36link Opportunity.IsWon,
37link Opportunity.StageName,
38link //etc ...
39link };
40link return new Repository(Opportunity.SObjectType, queryFields);
41link }
42link
43link public virtual IRepository getOppLineItemRepo() {
44link List<SObjectField> queryFields = new List<SObjectField>{
45link OpportunityLineItem.Description,
46link OpportunityLineItem.OpportunityId,
47link //etc
48link };
49link return new Repository(OpportunityLineItem.SObjectType, queryFields);
50link }
51link
52link //etc
53link}
54link
55linkpublic class RepoFactoryMock extends RepoFactory {
56link @testVisible
57link private static List<SObject> QueryResults = new List<SObject>();
58link @testVisible
59link private static List<Query> QueriesMade = new List<Query>();
60link
61link public override IRepository getOppLineItemRepo() {
62link List<SObject> queriedResults = this.getResults(OpportunityLineItem.SObjectType);
63link return queriedResults.size() > 0 ?
64link new RepoMock(queriedResults) :
65link super.getOppLineItemRepo();
66link }
67link
68link private List<SObject> getResults(SObjectType sobjType) {
69link List<SObject> resultList = new List<SObject>();
70link for(SObject potentialResult : QueryResults) {
71link if(potentialResult.getSObjectType() == sobjType) {
72link resultList.add(potentialResult);
73link }
74link }
75link return resultList;
76link }
77link
78link private class RepoMock implements IRepository {
79link private final List<SObject> results;
80link
81link public RepoMock(List<SObject> results) {
82link this.results = results;
83link }
84link
85link public List<SObject> get(Query query) {
86link return this.get(new List<Query>{ query });
87link }
88link
89link public List<SObject> get(List<Query> queries) {
90link QueriesMade.addAll(queries);
91link return this.results;
92link }
93link }
94link}
OK! So let's review the benefits we've gained from this structure and approach so far:
RepoFactoryMocks.QueryResult
where you can dump your expected query items across a wide swath of SObjects. With minimal boilerplate, the mock then decides per each IRepository override whether or not there are SObject results for each SObjectType and if a real or fake repository needs to be returnedAnd let's revisit our original test examples to see how they might look. Note again the use of the helper method to generate SObject Ids. I discussed this in the Mocking DML article previously:
1linkpublic class OpportunityUpdater {
2link private final IRepository oppLineItemRepo;
3link
4link public OpportunityUpdater(Factory factory) {
5link this.oppLineItemRepo = factory.RepoFactory.getOppLineItemRepo();
6link }
7link
8link public void updateOppsOnClose(List<Opportunity> updatedOpps) {
9link Map<Id, Opportunity> idtoUpdatedOpps = new Map<Id, Opportunity>(updatedOpps);
10link
11link Query oppQuery = new Query(Opportunity.Id, Query.Operator.EQUALS, idToUpdatedOpps.keySet());
12link List<OpportunityLineItem> lineItems = (List<OpportunityLineItem>)this.oppLineItemRepo.get(
13link oppQuery
14link );
15link for(OpportunityLineItem lineItem : lineItems) {
16link if(lineItem.Description == 'business logic criteria') {
17link Opportunity opp = idToUpdatedOpps.get(lineItem.OpportunityId);
18link opp.Description = 'Magic Business String';
19link }
20link }
21link //etc...
22link }
23link}
24link
25link//in your test class
26link@isTest
27linkprivate class OpportunityUpdater_Tests {
28link @isTest
29link static void it_should_update_opportunities_correctly() {
30link //arrange
31link Opportunity firstOpp = new Opportunity(Id = TestingUtils.generateId(Opportunity.SObjectType));
32link Opportunity secondOpp = new Opportunity(Id = TestingUtils.generateId(Opportunity.SObjectType));
33link List<Opportunity> opps = new List<Opportunity>{ firstOpp, secondOpp };
34link
35link OpportunityLineItem lineItem = new OpportunityLineItem(
36link OpportunityId = firstOpp.Id,
37link Description = 'business logic criteria'
38link );
39link
40link //act
41link RepoFactoryMock.QueryResults.addAll(opps);
42link RepoFactoryMock.QueryResults.add(lineItem);
43link Factory.getFactory().withMocks.getOpportunityUpdater().updateOppsOnClose(opps);
44link
45link //assert
46link System.assertEquals('Magic Business String', firstOpp.Description);
47link System.assertNotEquals('Magic Business String', secondOpp.Description);
48link }
49link}
Furthermore, because our RepoFactoryMock can tell us which queries were performed, we can easily add conditions to our OrderUpdater class and verify in the tests that the query has been updated correctly. Having a strongly typed method for comparing changes to SOQL queries is an extremely powerful tool in your toolbelt. Rather than validating that your query string has been typed correctly in raw SOQL, you can assert for that in your tests.
I've just gone through and outlined the most barebones Repository pattern implementation within Apex. It's easily extensible, and functionality can be increased with minimal method additions in clearly delineated places.
When I needed to apply a "LIMIT" statement to a query, that functionality was easy to add. When I needed to add sorting, that was achieved through the use of another enum within the Query class and the addition of a method to the Repository.get
method. Something that I have often thought of, though I haven't really had the need for it thus far, is re-implementing the common method seen in some mocking libraries that dictates to the mock how many results should be returned per function call. With the above implementation, it ends up being simple to add in an override on the RepoFactoryMock
to dictate just how many of the relevant results would be returned.
The combination of the Repository and Crud classes represents the entirety of what's necessary to supercharge your Apex unit tests; providing the benefit of both strongly-typed testing and blazing fast test speed. It should be noted that I actually espouse the use of three different factories —
This pattern is the result of many years and many iterations on similar themes across various Salesforce orgs. I haven't seen a verifiably better way that leads to the same decrease in testing time and object complexity, but I'm always looking for new ways to do better. In particular, the lack of generics in Apex makes for some frustrating casting of returned objects. It would be ideal if the below were the method signatures for IRepository:
1linkpublic interface IRepository<T> where T : SObject {
2link List<T> get(Query query);
3link List<T> get(List<Query> queries);
4link}
But hey, that's the world we live in. The day we get lambda functions and generics in Apex will be a very exciting one indeed. I hope you enjoyed this article. I know it's a long one, but I tried to find the right balance between verbosity and justification both in the code examples and prose. You can find full examples at the code over at my Apex Mocks repo, which I still haven't discussed in detail here but will in a coming post.
The long and short of it is that the use of the built-in Apex stubbing methods is not as performant as an approach like the one I'm detailing here.
Till next time!
The original version of The Repository Pattern can be read on my blog.
This entire post was written on two separate plane rides on 9 January 2020 from Boston, Massachusetts to Chicago and then Chicaco to Portland, Oregon. I did not have Wifi; an interesting challenge when trying to write a language that requires an internet connection in order to be compiled. Here are the changes I had to make to get the tests passing and the code to compile:
getDescribe().getName()
for the passed in SObjectFields, which led to a rather hilarious query exception to be thrown due to the @
sign in the SObjectField tokengetOperator
method to correctly reflect this. Switch statements can be very polarizing, as well, in Apex — they're more verbose than if statements, and the benefits you get with them are probably higher with SObjects; an enum switch statement takes up a lot of room, and only saves you the use of the full enum reference (IE Query.Operator.EQUALS
instead of just EQUALS
in the switch). Looking at that particular function again, I'd probably favor the if/else syntax to cut down on the lines of codeThe approach I've written about is a big improvement over the original implementations of these classes that I worked on years ago, and perhaps nothing better exemplifies that than the example test for the OpportunityUpdater
passing on the first go. Still, I knew that my examples were lacking several key functionality aspects — true support for Lists and Sets, in particular, and so the implementation that you'll find on the Apex Mocks repo is slightly different from the baseline implementation shown here.
If you made it this far, many thanks for taking the time to read, and I hope you enjoyed this post.
Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Formula Date Issues Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Replacing DLRS With Custom Rollup Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests