This is an article about the best practices to write Apex unit test code into Salesforce to get the better results. This requires to have a dev environment setup and ready. If you don’t have, follow this link!
First of all, I want to clarify the purpose of why I write this article about Apex unit tests best practices.
This article is more about understanding why we are doing unit tests into Salesforce (and into other language 😉 ) and how we should design them to make it helpful.
Why Apex unit tests in Salesforce?
“To reach the 75% required by SFDC to deploy my Apex Classes on PROD.”
I know I know guys, this is frustrating, especially if you’re in a hurry and… and…. and… 74% coverage :'(
In fact, Unit test is the only way, for the developer of a piece of code, to notify the team if any modification in the whole org makes this code not working anymore.
Let’s imagine you’ve written a beautyfull Apex function, called from an Apex Trigger, that updates all the products listed in an Opportunity to mark the sold date when the opportunity reaches the Stage “Closed / Won”.
It would be great to write an Apex Test class that:
- Verifies that when the stage changes, the Products are correctly updated.
- Fails if it’s not the case (and this point is very important, we’ll see it in the next paragraph)
That’s why, if you’re a technical leader / architect, you might put in place Apex Unit tests best practices.
ALWAYS use assertions in your Salesforce Apex test classes
For me, using Assertions in unit test classes should be mandatory.
Running the code is something good, but Assertions is the way we ENSURE that the code we have written gives the result that we expect.
That makes the code more robust.
Let’s take an Example. We want to use Salesforce to sell cars and we want to update custom field of the the product (the car sold) with the Date of today when it is referenced in an Opportunity that becomes closed / won.
The code would look be like that (I kept the code simple to be easily understandable, additionnal checks may be necessary):
// Verify the Opportunity with the good stage
List<Id> closedWonOpportunities = new List<Id>();
for(Opportunity loopOpp : updatedOpportunities){
if(loopOpp.StageName == 'Closed Won'){
closedWonOpportunities.add(loopOpp.Id);
}
}
// Get all the OLIs
List<OpportunityLineItem> lineItems = [SELECT Id, Product2Id FROM OpportunityLineItem WHERE OpportunityId IN :closedWonOpportunities];
List<Id> idOfProductsToUpdate = new List<Id>();
for(OpportunityLineItem looOli : lineItems){
idOfProductsToUpdate.add(looOli.Product2Id);
}
// Get the Products to update
List<Product2> productsToUpdate = [SELECT Id , Sold_date__c FROM Product2 WHERE Id IN :idOfProductsToUpdate];
// Update the Sold date to Today
for(Product2 loopProductToUpdate : productsToUpdate){
loopProductToUpdate.Sold_date__c = System.today();
}
update productsToUpdate;
Now, let’s see what the Test class looks like.
I added the data in the setup to make de code understandable.
@isTest
public with sharing class OpportunityHandlerTest {
private static final String OPP_NAME = 'OPP_NAME';
@TestSetup
static void makeData(){
Account acc = new Account();
acc.Name = 'TEST ACCOUNT';
insert acc;
Opportunity opp = new Opportunity();
opp.Name = OPP_NAME;
opp.AccountId = acc.Id;
opp.StageName = 'Open';
opp.CloseDate = System.today();
insert opp;
Product2 prod = new Product2();
prod.Name = 'PROD NAME';
insert prod;
PriceBookEntry pEntry = new PriceBookEntry();
pEntry.IsActive = true;
pEntry.UnitPrice = 100;
pEntry.Product2Id = prod.Id;
pEntry.Pricebook2Id = Test.getStandardPricebookId();
insert pEntry;
OpportunityLineItem oli1 = new OpportunityLineItem();
oli1.OpportunityId = opp.Id;
oli1.Quantity = 1;
oli1.TotalPrice = 200;
oli1.PricebookEntryId = pEntry.Id;
insert oli1;
}
@isTest
static void ensureProductUpdated(){
// Request the opportunity created in setup
Opportunity testOpp = [SELECT Id, StageName FROM Opportunity WHERE Name = :OPP_NAME];
// retrieve the Opportunity line items
List<OpportunityLineItem> lineItems = [SELECT Id, Product2Id FROM OpportunityLineItem WHERE OpportunityId = :testOpp.Id];
List<Id> listOfProductIds = new List<Id>();
for(OpportunityLineItem looOli : lineItems){
listOfProductIds.add(looOli.Product2Id);
}
// retrieve the corresponding products and verify the sold date is null
List<Product2> updatedProducts = [SELECT Id , Sold_date__c FROM Product2 WHERE Id IN :listOfProductIds];
for(Product2 loopProductToUpdate : updatedProducts){
Assert.isNull(loopProductToUpdate.Sold_date__c, 'The Sold date should be null');
}
// Now update the Opportunity
testOpp.StageName = 'Closed Won';
update testOpp;
// request the products again
updatedProducts = [SELECT Id , Sold_date__c FROM Product2 WHERE Id IN :listOfProductIds];
// check that the product is correctly updated
for(Product2 loopProductToUpdate : updatedProducts){
Assert.areEqual(System.today() , loopProductToUpdate.Sold_date__c, 'The Sold date should not set to Today');
}
}
}
In the above code, we can see that:
- We check first that the sold date is not set (the opportunity is in the Stage Open)
- After the update of the opportunity Stage, I check that the sold date have been set to the good value
So that if a developer add some more business logic on top of the existing, we make 100% SURE that the previously implemented logic remains working properly as initially implemented.
The Salesforce Ape test philosophy: an isolated environment
There is something that is simply AMAZING in the Salesforce Unit Test framework, it’s that when you run the test, all the Data you create / delete / update is completely independant from the data that reside in the org you are unning tests in.
What does it means? It means that you have the ability to create only the data you need, manipulate it, request it, the existing data will not affect the behavior of your test.
That’s something incredible. When you develop in Java or other language, it’s quite hard to have this type of way to work.
You have to setup it by yourself, run an H2 (or other lightweight) database of, but it’s not exactly the same database engine as in production… we don’t have this type of problems in Apex Unit Tests, and that’s a big advantage 🙂
Use Apex Test Data factories in you Salesforce
You’ll see, when you start writing Apex unit tests, you’ll see that you very often write the same creation of test datas.
So, it’s highly recommended to mutualize the creation of these test datas.
Let’s have a look on how we can get this done.
In the paragraph about assertions of this article, there is a “@testSetup makedata” method that create data. Let’s see how to transform these creations so that it will be reusable.
Create a Data factory Apex class for Account
Create a class an put the creation of the Account in the static method:
public with sharing class Test_DF_Account {
public static final String ACCOUNT_NAME = 'TEST ACCOUNT';
public static Account createSimpleAccount(){
Account acc = new Account();
acc.Name = ACCOUNT_NAME;
return acc;
}
}
Repeat the same for creating factories for Opportunities Pricebook…
I’ll not detail the creation of all the classes, only the one for opportunity and opportunity line item, but I’m sure you got the point on how to create test data factory classes 😉 !
The only thing is: don’t insert the data in the factory. The principle is to generate an object that is ready to be inserted in the test class to be implemented for the scenario you identified.
public with sharing class Test_DF_Opportunity {
public static final String OPP_NAME = 'OPP_NAME';
public static Opportunity createSimpleOpportunity(Account paramAccount){
Opportunity opp = new Opportunity();
opp.Name = OPP_NAME;
opp.AccountId = paramAccount.Id;
opp.StageName = 'Open';
opp.CloseDate = System.today();
return opp;
}
public static OpportunityLineItem createOLI(Opportunity paramOpportunity, PricebookEntry pEntry){
OpportunityLineItem oli1 = new OpportunityLineItem();
oli1.OpportunityId = paramOpportunity.Id;
oli1.Quantity = 1;
oli1.TotalPrice = 200;
oli1.PricebookEntryId = pEntry.Id;
return oli1;
}
}
Don’t use of the SeeAllData feature!
There is feature that we must talk about. It’s the @isTest(SeeAllData=True) annotation. This annotation makes the real data that is stored on the org visible from unit tests.
It’s a very very very bad practice to use it. In fact, you’ll be developing in a Sandbox that owns its own data.
You’re test will than be pushed to INTEGRATION, UAT, PRE-PROD sandboxes (depending on your release process) and all these sandboxes have their data that can be different from the data in the dev environment.
So, really, avoid to develop tests based on existing data, you’re nearly 100% sure these tests will start to fail because the data has changed.
Keep in mind that Salesforce OWD sharing rules applies in tests!
The sharing rules set up in your org will apply. So you may need to impersonate another user and so to run your code as if another user, with specific rights is trying to perform an action on the system.
For that, use the System.runAs method that takes a user in parameter and allows you to execute some code.
See some examples here.
Keep you Apex Unit tests as “unit” as possible
Unit tests are specifically designed to evaluate the behavior of individual units of code in isolation.
A unit refers to the smallest testable part of an application, typically a method or function. By adhering to the principle of small units, you ensure that each test focuses on a specific functionality or feature. This approach not only simplifies the testing process but also allows for easier debugging and maintenance.
It means that an Apex unit test best practice can be (as much as possible) writing one test per Apex method. So that, when a test starts to fail, finding the problem becomes easier since it covers only a specific piece of code!
If you need to test a method that is declared as private, no worries, just annotate this method with @testVisible and you’ll be able to access it directly in the Apex Test Class!
Use Apex Test.StartTest and Test.StopTest methods
Using Test.StartTest and Test.StopTest methods proves highly useful for resetting the governor limits in a test.
Imagine that you have to test code that nearly reaches the SFDC governor limits.
After that, you need to perform some additional SOQL queries to verify the results for example, but you have reached the limit. You test will failed although it works out of a test.
So, as an Apex unit test best practice, use the magic Test.StartTest and Test.StopTest to help you with the governor limits 🙂
Let’s see how to design the code of your test:
@isTest
public static void testAccountCreation(){
// Create objects
// Init things
// Then start the test
Test.startTest();
// Run code to test
// When all code to test hav been ran, stop the test
Test.stopTest();
// Verify things, do assertions
}