The Quest for the Holy Unit Test
In the Salesforce world, the term “Unit Test” is a matter of unimaginable pain and suffering. Although incredibly useful, they can cause a huge amount of headache unless they are carefully and logically crafted. Let's start with the basics.
Introduction
In the Japanese culture there is a particular kind of master craftsmen who hone their skills through years of experience as they perfect their woodworking trade. Those masters are able to assemble whole cabinets without using glue or even a single nail, using joints and different geometrical shapes to hold the pieces with friction only. Tolerances are extremely precise. A cabinet made by one of these masters is very interesting to observe since the drawers slide gently in and out on a cushion of air, enabled by a precise millimeter gap. Once you try to close one drawer, the pressure generated inside will cause another drawer to open on its own. Once you try to close this one, you will cause a third one to open and so on, until you finally manage to close all of them.
This immediately reminded me about something similar that we often see in the information technology industry. Something that each young engineer has experienced when it comes to fixing bugs. We try to fix one bug but the fix causes another bug to appear. This common occurrence is not generally a huge problem, as long as this new bug is identified and fixed before being released into the production environment. In order to minimize the impact of those additional, unknown bugs, unit tests have become the default mechanism to identify and prevent unintended consequences. Unit tests automate various scenarios through the code, compare results with expected outcomes, and verify that it worked correctly.
Inside of Salesforce you would have to write those unit tests, where outside of Salesforce they are only an option. The question remains whether you should write them or not. The fact is that unit tests can take a huge amount of time to write and a lot of companies are not willing to invest in them. Writing them, from my perspective, is a very recommended, yet ungrateful work. If you write them and they all pass you have no feedback on if they were truly worth it. Once you get to 80% of project completion and change requests or bugs start to show up, you realize how great they are at preventing you from causing an unintended disaster.
Now imagine yourself starting a project and you have the choice of writing unit tests. You can either have them or not. If you do then you can choose to do them correctly. If you write them correctly you will have to invest a lot of extra time. If you write them only to get the code coverage percentage then you would be better off to skip them entirely since you are investing time without any benefit.
In Salesforce (and I am really not sure whether to say fortunately / unfortunately), you do not have the option to choose. You have to write them with a coverage of at least 75%. What we see very often is that the code written has only been covered by tests but not tested at all. So the real question is “WHY?”
Let’s ask a short question. What does a typical piece of code look like? Does it look like the following example?
This. Is. A. Single. Method.
Here we have a 400 lines of code method, 5 levels of nested if statements, 4 levels of for loops, countless helper maps and sets, SOQL queries mixed up with business logic code.
How do you test this kind of code in a meaningful way, especially when you have to load 20 different objects into the database just to be able to test the code. Recently Salesforce strengthened the limit enforcement in unit tests. What happens when not even restarting the test scope helps because of the crazy amount of triggers involved?
The answer to the “WHY” question is actually very simple. Unit tests have been around in the IT industry for quite some time and have standard procedures on how to write them. Those procedures have never been established in Salesforce and a huge number of developers have never worked with other well established technologies (.NET, Java...). The majority of Salesforce developers have never heard about design patterns or the SOLID principles, while in the rest of the IT industry, those terms are being learned as a junior developer, truly understood as a mid level developer, and defended in arguments as a senior.
An Example
To understand how to write unit tests correctly it is important to understand the SOLID principles, particularly the last one, “Dependency Inversion Principle“.
Let’s imagine you have a piece of code that is executing some business logic while querying the database and doing various updates.
public void foo(List<Account> accounts) {
Set<ID> accountIds = new Set<ID>();
for (Account a : accounts)
accountIds.add(a.Id);
List<Contact> contacts = [
SELECT Id, FirstName, LastName
FROM Contact
WHERE FullName__c = null AND
AccountId IN :accountIds
];
for (Contact c : contacts)
c.FullName__c = c.FirstName + ' ' + c.LastName;
UPDATE contacts;
}
This is a very simple code snippet example, but already we can see several questionable issues. Nearly every Salesforce developer has seen samples like this implemented in an environment or project.
The first issue I have with this are lines 2-4. It is a simple ID extraction, yet we can see similar code hundreds of times in every project. A simple fix would be to extract it into a new method and use the SObject type. We could even go a step further to utilize dynamic APEX and make it viable for any data type.
public static Set<ID> extractIds(List<SObject> sos) {
Set<ID> ids = new Set<ID>();
for (Sobject so : sos)
ids.add(so.Id);
return ids;
}
public void foo(List<Account> accounts) {
List<Contact> contacts = [
SELECT Id, FirstName, LastName
FROM Contact
WHERE FullName__c = null AND
AccountId IN :extractIds(accounts)
];
for (Contact c : contacts)
c.FullName__c = c.FirstName + ' ' + c.LastName;
UPDATE contacts;
}
What we have done here is created a static method that can be utilized whenever needed. It should be extracted into some kind of a utility class that you can reuse. Please note that we also have put the execution of the static class into the SOQL query. But now comes a much bigger problem.
When you execute a unit test in Salesforce, a virtual blank instance of the database is being initiated. This only takes a few milliseconds, but keep in mind that when you execute a deployment this is done for every unit test method, for each class, and all triggers are executed for each stored record. If you have 15,000 unit tests, the time can easily total 5+ hours, just to complete with a message of a failed deployment. At this point, you cannot do anything concerning whether the deployment will pass, but what you can do is to get that crucial, time constrained, feedback ASAP.
In the rest of the software development industry you will never see unit tests inserting data into the database, but how can we prevent it here while still being able to test our business logic code? If we would start the foo() method unit test without storing any data inside, then we would never be able to cover/test line 19, which is the core of the business logic here. The answer is to bring into the game a data provider class, often called a repository.
// Repository class
public class Repository {
public class ContactRepository extends RepositoryBase {
public List<Contact> getByAccount(Set<ID> accountIds) {
return [
SELECT Id, FirstName, LastName
FROM Contact
WHERE FullName__c = null AND
AccountId IN :accountIds
];
}
}
public class RepositoryBase {
public void updateObjects(List<SObject> sos) {
UPDATE sos;
}
}
}
// Utility class
public static Set<ID> extractIds(List<SObject> sos) {
Set<ID> ids = new Set<ID>();
for (Sobject so : sos)
ids.add(so.Id);
return ids;
}
// Some random business logic class method
public void foo(List<Account> accounts) {
Repository repoContact = new Repository.ContactRepository();
List<Contact> contacts =
repoContact.getByAccount(extractIds(accounts));
for (Contact c : contacts)
c.FullName__c = c.FirstName + ' ' + c.LastName;
repoContact.updateObjects(contacts);
}
What we have done is created a data provider class, whose whole reason for existence is to push and pull the data within the database as a middleman. Our business logic (lines 35-36) code has shrunk quite a bit, yet still we did not write anything that would make it easier to write a unit test. In line 33 we initiate a DataRepository class instance, inside of which we have full control over which object will be instantiated.
Now we have come to the SOLID part (you can also google “Inversion of Control” (IOC) and “Dependency Injection” (DI).
In the beginning we had a single method that was doing everything. All lines of code are very tightly coupled and interconnected. Since this code is only a part of a bigger business logic, a single change request could easily make this method extremely complex.
Afterwards we refactored and extracted independent units into small chunks, but still those chunks are tightly coupled with our business logic code. Inside of the business logic part it is strictly controlled which data repository is being used.
Next we need to create a ContactRepo repository and have an external factory method return an instance of the repository interface member.
// Repository class
public class Repository {
public interface IRepositoryBase {
void updateObjects(List<SObject> sos);
}
public interface IContactRepository extends IRepositoryBase {
List<Contact> getByAccount(Set<ID> accountIds);
}
public class ContactRepository extends RepositoryBase
implements IContactRepository {
public List<Contact> getByAccount(Set<ID> accountIds) { ... }
}
public class RepositoryBase {
public void updateObjects(List<SObject> sos) { ... }
}
public IContactRepository ContactRepo_Mock { get; set; }
public static IContactRepository getContactRepo() {
IContactRepository repo = new Repository.ContactRepository();
if (Test.isRunningTest() && ContactRepo_Mock != null) {
repo = ContactRepo_Mock;
ContactRepo_Mock = null;
}
return repo;
}
}
// Utility class
public static Set<ID> extractIds(List<SObject> sos) {
Set<ID> ids = new Set<ID>();
for (Sobject so : sos)
ids.add(so.Id);
return ids;
}
// Some random business logic class method
public void foo(List<Account> accounts) {
Repository.IContactRepository repoContact =
new Repository.getContactRepo();
List<Contact> contacts =
repoContact.getByAccount(extractIds(accounts));
for (Contact c : contacts)
c.FullName__c = c.FirstName + ' ' + c.LastName;
repoContact.updateObjects(contacts);
}
The goal of unit testing is to automate the safeguarding of business logic and prevention of accidental bugs. This task becomes significantly easier if we split complicated logic into small chunks and test them separately and as a whole. Small, reusable methods that are easy to test and organize. The highlighted factory method will by default return the right repository, but you can always override it with another class instance of your choice.
So how do we test this code? First, in the test class, we have to create a subclass that will implement the ContactRepo and we can even hardcode some simple mechanisms.
@IsTest
private class FooClassTest {
@IsTest
private static void loadRelatedLists() {
// test setup
Mock_ContactRepo mock = new Mock_ContactRepo();
Repository.ContactRepo_Mock = mock;
// test execution
foo();
// assert
System.assert(mock.isQueried);
System.assert(mock.isUpdated);
}
private class Mock_ContactRepo implements Repository.IContactRepository {
public Boolean isQueried = false;
public Boolean isUpdated = false;
public List<Contact> getByAccount(Set<ID> accountIds) {
isQueried = true;
return new List<Contact>{
new Contact(FirstName = 'test', LastName = 'test')
};
}
public void updateObjects(List<SObject> sos) {
isUpdated = true;
}
}
}
As you can see, we have a mock class that is a subclass inside of the unit test. Previously we had the foo() method initiating a concrete DataProvider class and then we abstractified it to ask a factory method to return a DataProvider instance of some abstract interface member. In the production run, it will get a database access ability, but in the unit test it never touches the database. We can effectively test the business logic code by expanding the mock class to temporarily store the contacts so that you can assert them.
The implications of such an approach is that you always have one class that deals with the database queries. If some field is missing, you know immediately where to look for it. Frequently, the maintenance of unit tests in the midst of change requests becomes a real nightmare. Imagine a method that has 200 lines of code. Its corresponding unit test could easily have doubled that amount. If you get lost in 200 lines of business logic code imagine how lost you will get with 400, especially when you have little idea what is going on. This approach will also help your tests require significantly fewer lines of code. Creating such a repo is additional work up front, but remember that you only need to do it only once and then you can reuse it multiple times. If you have to invest time in unit tests, at least make it worthwhile.
The biggest benefit in this case is actually the deployment speed. No database is instantiated, no triggers run, nothing is stored, and no processes are kicked off. You only test the code you wrote. We had a real world example for this purpose where the client wanted a clean start from scratch on a new org. We rewrote the whole org in this way to match the processes. After 9 months of programming the deployment time fell down from 120 minutes to just 4. After two years of code use and updates it completed quickly in under 8 minutes. The implication of this is that you can deploy fixes several times in an hour. If a quickfix does not work out, you can have a new one in minutes up and running, reducing your maintenance costs dramatically.
There are several mistakes that people make when writing unit tests.
They try to unit test huge chunks of code.
The term “Unit Test” has the prefix “Unit” for a reason, because you are supposed to test small chunks of code that do a single thing and nothing but that (now I realise that we should write another blog post about the SOLID principles). If you test small chunks and they work perfectly, then you do not need to test them again to see other code utilize them. You only care if the other code has executed it in the correct order.Other people may ask, “What about flows, business process flows, etc.?”. The answer is “You do not care about them.'' They are separate units which are not this unit. If you also want to test them then you should consider writing a separate test. Consider them as an integration test and do not include them here.
People may say “Yes, but what about the SObject fields that are rollup summaries and formulas, they cannot be set to a value and I’ll have to insert data into the database”. No, you can specify those values by utilizing JSON and other mechanisms. We have also completed libraries that take care of it. We will soon publish them and come to those code samples in the near future.
Some other people would immediately see a problem with using ID types and lookups, but also for this case, we have libraries that make a really easy job of faking those values for any object without hardcoding anything. Those will be published soon as well.
Salesforce is forcing you to write unit tests for the code you write and very rarely are those tests worth anything. It becomes very hard to have meaningful unit tests on heavily used and customised objects like Account, Opportunity, Quote, Order and Product. Many teams come into the project and modify things their own way, while mostly caring only about deploying their own code. Let’s assume this team made some modification to some existing functionality, using the usual development where business logic and queries are mixed. Once they deploy those changes, which are not easy to maintain, they will know the details of this logic and code now but in 6 months not even they will. In other well established programming languages you will find guidelines on how to write easily maintainable code (If you can take a look at it and understand the functionality in 10 seconds, then it is good code), but in the Salesforce world, those practices are mostly missing. Because of this, unit tests need to be transparent, easily readable, and useful. If your unit test suite does not satisfy all 3 criteria you will get yourself into trouble and your tests will become useless, yet the most efficient way to get good and easy maintainable tests unit tests is to write simple and solid code.
Those practices have been well established in the software industry for decades.yet still fail to appear more often in the Salesforce development world. APEX is a limited language, since it does not support generics, yet it is still possible to accomplish it.
The example code I used here is a simplification of the technique for demonstration purposes, that we use on a daily basis. From this, it has evolved over the years to be more usable and intuitive. We are planning to release it shortly as an OpenSource project.
I hope you will have fun with this code and this alternative approach to unit testing and please let us know what you think!