You’ve crafted some great Apex code for your org, and you want to create some clean, efficient unit tests. Often your tests will need records with some Record IDs for lookup or parent relationships populated, so you end up inserting extra records in the database and querying them back just to get the IDs, which seems an unnecessary step.
Writing records to the database and then reading them back takes time. Lots of time
On top of that you often have multiple tests that need the same or similar data. So your tests end up creating the same large number of records again and again, and take a long time to run.
Also, you often have to create records for multiple linked objects that are not directly required for your tests. For example, if an Order needs an Account in order to be inserted into the database, then you will need to insert an Account before inserting the Order even if your test doesn’t use the AccountId.
In some cases multiple records may be required, for example to insert an OpportunityLineItem may require a Product, PriceBook and PriceBookEntry to be created before the OpportunityLineItem can be written to the database
You end up creating records for each parent object in a master/detail or required lookup relationship. This can vastly increase the number of records that need to be added to the database, most of which are not needed for your tests.
Surely there’s a better way that avoids the database, creating your test records in memory with all the required related Record IDs.
Maybe there is a way! Let’s take a look, starting with what makes up a Salesforce Record ID.
A Record ID in Salesforce is comprised of a number of parts.
These are:-
In the Summer 23 release, the Instance ID changed from 2 to 3 characters in length – previously it was the 4th and 5th characters, but since then it has been the 4th, 5th and 6th characters of the ID. As the Instance ID increased in length, it took the first of the reserved characters.
For more information, refer to this helpfulful thread on Salesforce Stack Exchange, or this handy guide by Shivan Vishwakarma: Demystifying “Salesforce IDs”.
You can assign a Record ID to an instance of an object class in memory, but it has to have the correct prefix for that object. And these prefixes for custom objects can vary from your live org to your sandboxes and scratch orgs.
The prefix can be retrieved in Apex using the Schema class.
Schema.getGlobalDescribe().get(objectName).getDescribe().getKeyPrefix();
In this example, objectName
is the API name of any object in the org, e,g, Account, My_Object__c or pkg__Other_Object__c.
The Instance ID needs to be a valid Instance ID. You can take the 4th, 5th and 6th characters from the Record ID of a recently created record in your live org, sandbox or scratch org . Alternatively, the Instance ID can be zeros.
The index number part is an 8 character string. I favour using this part of the Record ID having something that represents the name of the object. Some people might be able to remember the prefix for all the objects they use, but I can’t. I take the label of the object and convert it into a string padded out to 9 characters.
For example:-
The Record IDs you create don’t have to be unique, but it will help distinguish between different test records if they are. I use the suffix to contain a 3 digit number to make each generated ID different. This will handle up to 999 Record IDs, which should be enough for any single test.
Here’s a small class that will create a dummy Record ID for a particular object.
/**
* Class to create dummy record IDs for use when no database access is desired.
*
* Each record ID, even dummy ones, must have the correct prefix
* for the object to which it will be assigned.
*/
public class TestObjectIdFactory {
private static Integer index = 1;
private static Map<String, String> objAbbrevByPrefix =
new Map<String, String>();
/**
* Create an 18 character ID string from the object prefix,
* a three digit instance ID (zeros),
* a 9 character abbreviation of the object name and a 3 digit index number.
*/
public static String createId(String objectName) {
String idPrefix = getObjectIdPrefix(objectName);
String abbreviation = determineAbbreviation(objectName, idPrefix);
return idPrefix + '000' + abbreviation
+ String.valueOf(index++).leftPad(3, '0');
}
/**
* Retrieve the correct 3 character prefix of IDs for a given object.
*/
public static String getObjectIdPrefix(String objectName) {
return Schema.getGlobalDescribe()
.get(objectName).getDescribe().getKeyPrefix();
}
/**
* Determine a 9 character representation of an object label,
* right padded with 'x' to bring it to 9 characters, if necessary.
*/
private static String determineAbbreviation(String objectName, String prefix) {
String label = Schema.getGlobalDescribe().
get(objectName).getDescribe().getLabel();
String abbreviation = objAbbrevByPrefix.get(prefix);
if (abbreviation == null) {
String[] parts = label.split(' ');
abbreviation = '';
Integer len = 9 / parts.size();
for (String part : parts) {
abbreviation += len < part.length() ? part.substring(0, len) : part; } if (abbreviation.length() > 9) {
abbreviation = abbreviation.substring(0, 9);
}
abbreviation = abbreviation.rightPad(9, 'x');
objAbbrevByPrefix.put(prefix, abbreviation);
}
return abbreviation;
}
}
You can use it in a test class like this:
Order testOrder = new Order(
Id = TestObjectIdFactory.createId('Order'),
Name = 'Test Order',
OpportunityId = TestObjectIdFactory.createId('Opportunity'),
My_Custom_Object__c = TestObjectIdFactory.createId('My_Custom_Object__c'),
…
);
I hope you can see that it is possible to avoid using the database when requiring Record IDs for use in Apex tests. But why would you want to do this?
Writing records to the database and reading them back takes time. Adding records to the database and reading them back can result in tests taking much longer to run than creating the records in memory as described above. In some cases where there are several required lookup relationships, it has taken 4 times longer to run the same test when accessing the database compared to using the method described here.
If you have a large number of tests, this can mean a substantial amount of time being saved when you run the tests such as when deploying to your live org.
While this can help reduce the time taken to run your unit tests, you shouldn’t avoid writing records to the database entirely. It is still important to run at least one test that actually uses the database to ensure that your code will work as expected in your live org.
I hope you find this as useful as I have.
Our independent tech team has been servicing enterprise clients for over 15 years from our HQ in Bristol, UK. Let’s see how we can work together and get the most out of your Salesforce implementation.