Using AAA pattern in salesforce unit tests
Salesforce requires at least 75% test coverage for production org. But a code coverage is not straightforward and really truth metric, because you can have 100% code coverage, but you can't be sure that it really tests all needed pieces of your app. Really, I saw a codebases which had 96-97%, but it was covered with a hack or it used AssertionFreeTesting. The main thing that you should keep in mind is that code coverage is not about quality of you code, is about a not covered pieces of you code. And you should cover it. I have used AAA(Arrange-Act-Assert) patter for apex unit tests and it make sense for me. Let's take a look what is means this pattern.
To follow this pattern you should do the following things:
Arrange all necessary preconditions and inputs.Act on the object or method under test.Assert that the expected results have occurred.
Let's deep inside each point with some demo preconditions:
Imagine yourself that you have such class which runs from Account trigger and made some updates on the related contacts (lookup field Executive__c).
trigger Account on Account (after update) {
Set<Id> contactIds = new Set<Id>();
for (Account account: trigger.new) {
contactIds.add(account.Executive__c.Id);
}
if (ReindexerConfig__c.getInstance().executiveReindexEnabled__c) {
AccountTriggerHandler.reindexExecutives(contactIds);
}
}
public class AccountTriggerHandler {
public static void reindexExecutives(Set<Id> contactId) {
// do something
}
}
So, we have some data which should be initialized before we might perform testing. The first thing is that we need Account and Contact with right data and the second one is that we need custom setting ReindexedConfiguration__c. We have 3 options:
- Create all needed data during test runtime
- Load test data with Test.loadData
- Provide access to real data with IsTest(SeeAllData=true) annotation (It's bad idea)
For me the first option is more convenient and I'll go ahead with it. So, I create a dedicated helper class for preparation a needed test data.
@isTest
public class TestHelper {
public static final String USER_NAME = 'TestUser_';
public static final Integer DEFAULT_NAME_FIELD_LENGTH = 80;
private static String generateUniqueName(String args, Integer maxLength){
String result = '';
for (Integer i = 0; i < args.size(); i++) {
result += ((args[i].replace(' ','_')) + '_');
if (result.length() >= (maxLength – 5)) {
return (result.substring(maxLength – 5) + (Integer)(Math.random() * 10000));
} else if (i+1 == args.size()) {
return (result + (Integer)(Math.random() * 10000));
}
}
return result;
}
private static String generateUniqueName(String name) {
return generateUniqueName(new String{name}, DEFAULT_NAME_FIELD_LENGTH);
}
private void initTestEnvironment() {
ReindexerConfig__c reindexConf = new ReindexerConfig__c();
reindexConf.executiveReindexEnabled__c = true;
insert reindexConf;
}
private static Id getProfile(String profileName) {
Profile profile = [
SELECT Id, Name FROM Profile WHERE Name =: profileName
];
return profile.Id;
}
private static Id getRoleId(String roleName) {
List<UserRole> roles = [
SELECT Id FROM UserRole WHERE Name =: roleName
];
if (!roles.isEmpty()) {
return roles[0].Id;
}
return null;
}
public User initTestEnvironementAsUser(String params) {
User resultUser = createTestUser(params[0],params[1]);
System.runAs(resultUser) {
initTestEnvironment();
}
return resultUser;
}
public Account createAccount() {
return new Account(Name = 'Test Account');
}
public Contact createContact() {
return new Contact (
FirstName = 'Test',
LastName = 'Contact'
);
}
public Contact createContact(Account account) {
Contact result = createContact();
result.Account = account;
return result;
}
public static User createTestUser(String profileName, String roleName){
String testUserName = generateUniqueName(USER_NAME);
User newUser = new User(
LastName = testUserName,
FirstName = testUserName,
Alias = testUserName.left(8),
Email = testUserName + '@mail.com',
Username = testUserName + '@mail.com',
CommunityNickname = testUserName,
UserRoleId = getRoleId(roleName),
TimeZoneSidKey = 'America/New_York',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'ISO-8859-1',
ProfileId = getProfile(profileName),
LanguageLocaleKey = 'en_US'
);
insert newUser;
return newUser;
}
}
Using helpers methods into test class
@isTest
private class AccountTriggerTest {
..
// Arrange section ———————————-
private static void initTestSuite() {
account = TestHelper.creteAccount();
contact = TestHelper.createContact(account);
}
}
// Act section ————————————–
// here we performs all needed action
private static void testExecutiveReindex() {
insert account;
account.SomeField__c = 'test';
update account; // run account trigger on update
}
private static void performTest2() {
// test something else
}
// Assert section ———————————–
// here we perform all assertion on
@isTest
private static void verifyExecutiveReindexAs() {
System.runAs(TestHelper.initTestEnvironementAsUser(COMMON_BUSINES_USER_CONF)) {
initTestSuite();
System.assert('Assert something on your objects as COMMON_BUSINES_USER_CONF');
System.assert('Assert any system state');
testExecutiveReindex(); // run "Act"
System.assert('Assert something on your objects as COMMON_BUSINES_USER_CONF');
System.assert('Assert any system state');
}
}
@isTest
private class AccountTriggerTest {
private static Account account;
private static Contact contact;
// users config for loading this configs into tests 'Profile' 'Role'
private static final String COMMON_BUSINES_USER_CONF = new String {'Common User','Branch Store'};
private static final String CHIEF_SECURITY_OFFICER_CONF = new String {'CSO','Top Level'};
private static final String BUSINES_ADMINISTRATOR_CONF = new String {'Busines Admin','Top Level'};
// Arrange section ———————————-
private static void initTestSuite() {
account = TestHelper.creteAccount();
contact = TestHelper.createContact(account);
}
// Act section ————————————–
// here we performs all needed action
private static void testExecutiveReindex() {
insert account;
account.SomeField__c = 'test';
update account;
}
private static void performTest2() {
// test something else
}
// Assert section ———————————–
// here we perform all assertion on
@isTest
private static void verifyExecutiveReindexAs() {
System.runAs(TestHelper.initTestEnvironementAsUser(COMMON_BUSINES_USER_CONF)) {
initTestSuite();
System.assert('Assert something on your objects as COMMON_BUSINES_USER_CONF');
System.assert('Assert any system state');
testExecutiveReindex();
System.assert('Assert something on your objects as COMMON_BUSINES_USER_CONF');
System.assert('Assert any system state');
}
}
@isTest
private static void verifyExecutiveReindexAs() {
System.runAs(TestHelper.initTestEnvironementAsUser(CHIEF_SECURITY_OFFICER_CONF)) {
initTestSuite();
System.assert('Assert something on your objects as CHIEF_SECURITY_OFFICER_CONF');
System.assert('Assert any system state');
testExecutiveReindex();
System.assert('Assert something on your objects as CHIEF_SECURITY_OFFICER_CONF');
System.assert('Assert any system state');
}
}
@isTest
private static void verifyExecutiveReindexAs() {
System.runAs(TestHelper.initTestEnvironementAsUser(BUSINES_ADMINISTRATOR_CONF)) {
initTestSuite();
System.assert('Assert something on your objects as BUSINES_ADMINISTRATOR_CONF');
System.assert('Assert any system state');
testExecutiveReindex();
System.assert('Assert something on your objects as BUSINES_ADMINISTRATOR_CONF');
System.assert('Assert any system state');
}
}
}
This pattern fills good for testing applications with complex and ramified hierarchy of users, roles and profiles.