Read more at jamessimone.net, or return to the homepage





The Repository Pattern

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.

linkThe Problem With SOQL Usage In Large Codebases

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.

linkImplementing the Repository Pattern in Apex

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.

classes/OrderUpdater.cls
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:

  1. We've introduced the need for another query; we're either going to have to introduce a new class or create a new selector method that's specific to this implementation (to filter for Closed Won opps, or to pull back the line items with the Opportunities as a child of the query). We're going to have to deal with a bunch of messy iterations in our code. Yes, we can hide the specifics by refactoring the inner body of verbose methods like 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.
  2. We've introduced a subtle regression in the existing code; a regression that would likely only be detected by the astute manager or salesperson in production. Did you spot it? Now that the 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.
  3. We've had to move OppLineItemRepoMock out to its own class, and we've raised the visibility of the class as a result. One possible way around this is through sharing a Mock class:
classes/MockFactory.cls
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.

linkComposing repository queries

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:

classes/Repository_Tests.cls
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:

classes/Query.cls
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).

linkCreating the Repository

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:

And 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.


linkWrapping up

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.


linkPostscipt

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:

The 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.

The Problem With SOQL Usage In Large CodebasesImplementing the Repository Pattern in ApexComposing repository queriesCreating the RepositoryWrapping upPostscipt

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 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 Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests



Read more tech articles