Salesfore Apex Test Class Best Pracitces

Apex Unit Tests

The Apex testing framework enables you to write and execute tests for your Apex classes and triggers on the Lightning Platform. Apex unit tests ensure high quality for your Apex code and let you meet requirements for deploying Apex.

Salesforce recommends that you write tests for the following:

Single action
Test to verify that a single record produces the correct, expected result.

Bulk actions
Any Apex code, whether a trigger, a class or an extension, may be invoked for 1 to 200 records. You must test not only the single record case, but the bulk cases as well.

Positive behavior
Test to verify that the expected behavior occurs through every expected permutation, that is, that the user filled out everything correctly and did not go past the limits.

Negative behavior
There are likely limits to your applications, such as not being able to add a future date, not being able to specify a negative amount, and so on. You must test for the negative case and verify that the error messages are correctly produced as well as for the positive, within the limits cases.

Restricted user
Test whether a user with restricted access to the sObjects used in your code sees the expected behavior. That is, whether they can run the code or receive error messages.

1. Test Class Utility

Use utility class for creating records for the test class.


Sample Utility class:

1.   @isTest  
2.   public with sharing class TestDataFactory {  
3.         
4.       public static List < Lead > createLeadGenLeads( Integer intCount, Boolean doInsert ) {  
5.             
6.           List < Lead > listLeads = new List < Lead >();  
7.           Id leadGenRecordTypeId = Schema.SObjectType.Opportunity.getRecordTypeInfosByName().get('Lead Generation').getRecordTypeId();  
8.             
9.           for ( Integer i = 0; i < intCount; i++ ) {  
10.               
11.             Lead newLead = new Lead() ;  
12.             newLead.FirstName = 'Test ' + i;  
13.             newLead.LastName = 'Sample ' + i;  
14.             newLead.Company = 'Clayton Test ' + i;  
15.             newLead.Status = 'Contacted';  
16.             newLead.RecordTypeId = leadGenRecordTypeId;  
17.             /* 
18.               Add all fields required for Lead Gen 
19.             */  
20.             listLeads.add(newLead);  
21.               
22.         }  
23.            
24.         if(doInsert)  
25.             insert listLeads;  
26.           
27.         return listLeads;  
28.           
29.     }  
30.       
31.     public static List < Lead > createRetailLeads( Integer intCount, Boolean doInsert ) {  
32.           
33.         List < Lead > listLeads = new List < Lead >();  
34.         Id retailRecordTypeId = Schema.SObjectType.Opportunity.getRecordTypeInfosByName().get('Retail').getRecordTypeId();  
35.           
36.         for ( Integer i = 0; i < intCount; i++ ) {  
37.               
38.             Lead newLead = new Lead() ;  
39.             newLead.FirstName = 'Test ' + i;  
40.             newLead.LastName = 'Sample ' + i;  
41.             newLead.Company = 'Claton Sample ' + i;  
42.             newLead.Status = 'Contacted';  
43.             newLead.RecordTypeId = retailRecordTypeId;  
44.             /* 
45.               Add all fields required for Retail 
46.             */  
47.             listLeads.add(newLead);  
48.               
49.         }  
50.           
51.         if(doInsert)  
52.             insert listLeads;  
53.           
54.         return listLeads;  
55.           
56.     }  
57.       
58. }  

To call the utility class methods in the test class, check the below sample code.

List < Lead > listLeadGenLeads = TestDataFactory.createLeadGenLeads(3, true);//This will insert and return the records.
List < Lead > listRetailLeads = TestDataFactory.createRetailLeads(3, false);//This will just return the 3 leads without any DML.

2. Set Up Test Data for an Entire Test Class in Salesforce

To Set Up Test Data for an Entire Test Class in Salesforce, @testSetup is used. @testSetup avoids creation of same set of records to be used in different test methods in the same test class.

Sample Test Class:

1.   @isTest  
2.   private class CommonTestSetup {  
3.     
4.    /* Method to setup data */  
5.       @testSetup static void setup() {  
6.     
7.           /* Create common test Accounts */  
8.           List < Account > testAccts = new List < Account >();  
9.           for ( Integer i = 0; i<2; i++ ) {  
10.             testAccts.add(new Account(Name = 'TestAcct'+i));  
11.         }  
12.         insert testAccts;       
13.   
14.    /* Create common test Contacts */  
15.         List < Contact > testContacts = new List < Contact >();  
16.         for ( Integer i = 0; i<2; i++ ) {  
17.             testContacts.add(new Contact(FirstName = 'TestAcct'+i, LastName = 'TestAcct'+i));  
18.         }  
19.         insert stName = 'TestAcct'+i;    
20.     }  
21.    
22.     @isTest static void testMethod1() {  
23.       
24.         /* Testing Method with Accounts and Contacts */  
25.           
26.     }  
27.    
28.     @isTest static void testMethod2() {  
29.       
30.         /* Testing Method with Contacts */  
31.     
32.     }  
33.   
34. }  

3. Use Test.startTest() and Test.stopTest()

Test.startTest() and Test.stopTest() are very useful when your test class hits Salesforce Governor Limits.

The code inside Test.startTest() and Test.stopTest() have new set of Salesforce Governor Limits. As a good practice, make sure initializing the variables, fetching records, creating and updating records are coded before Test.startTest() and Test.stopTest() and calling the controllers for code coverage is done inside Test.startTest() and Test.stopTest(). The code before Test.startTest() and after Test.stopTest() have new set of Salesforce Governor Limits and code between Test.startTest() and Test.stopTest() have new set of Salesforce Governor Limits.

Sample Test Class:

1.   private class TestClass {  
2.     
3.           static testMethod void test() {  
4.             
5.               /* 
6.               Declare the variables, Fetch the required records, Create and Update sample records 
7.               */  
8.                 
9.               Test.startTest();  
10.             /* 
11.             Call the controller for code coverage 
12.             */  
13.             Test.stopTest();  
14.               
15.         }  
16.           
17. }  

Note:
Any code that executes after the stopTest method is assigned to the original limits that were in effect before startTest was called.

4. System.assert, System.assertEquals and System.assertNotEquals

System.Assert accepts two parameters, one (mandatory) which is the condition to test for and the other a message (optional) to display should that condition be false.
System.AssertEquals and System.AssertNotEquals both accepts three parameters; the first two (mandatory) are the variables that will be tested for in/equality and the third (optional) is the message to display if the assert results in false.

Both are used in Test Class for “Positive Case Testing” and “Negative Case Testing” with single and multiple records.

Sample Code:

Trigger:

1.   trigger AccountTrigger on Account ( before insert ) {  
2.     
3.       for ( Account acct : trigger.new ) {  
4.             
5.           if ( acct.Name == 'Test Account' )  
6.               acct.Description = 'Test';  
7.                 
8.       }  
9.         
10. }  

Test Class:

1.   @isTest  
2.   private class SampleTestClass {  
3.     
4.       static testMethod void insertAcctTest() {  
5.         
6.           Account acc = new Account(Name = 'Test Account');  
7.           insert acc;  
8.           acc = [ SELECT Description FROM Account WHERE Id = : acc.Id ];  
9.           System.assertEquals('Test', acc.Description);  
10.           
11.     }  
12.       
13. }  

In the above example, the expected Description of the Account is 'Test'. If any other additional processes like Workflow Field update, Process Builder, etc updates the Description other than 'Test', it will throw an error and fails the test method. This will make sure that developed code is working as expected.

Opposite to System.assertEquals() is System.assertNotEquals().

5. runAs Method

Use the runAs method to test your application in different user contexts.
The system method runAs enables you to write test methods that change either the user contexts to an existing user or a new user. When running as a user, all of that user's record sharing is then enforced. You can only use runAs in a test method. The original system context is started again after all runAs test methods complete.

The following items use the permissions granted by the user specified with runAs running as a specific user:
Dynamic Apex
Classes using with sharing or without sharing
Shared records
The original permissions are reset after runAs completes.
The runAs method ignores user license limits. You can create new users with runAs even if your organization has no additional user licenses.

In the following example, a new test user is created, then code is run as that user, with that user's record sharing access:

1.   @isTest  
2.   private class TestRunAs {  
3.     
4.      public static testMethod void testRunAs() {  
5.        
6.           String uniqueUserName = 'standarduser' + DateTime.now().getTime() + '@testorg.com';  
7.             
8.           Profile p = [ SELECT Id FROM Profile WHERE Name = 'Standard User' ];  
9.             
10.         User u = new User(Alias = 'standt', Email='standarduser@testorg.com',  
11.         EmailEncodingKey = 'UTF-8', LastName = 'Testing', LanguageLocaleKey = 'en_US',  
12.         LocaleSidKey = 'en_US', ProfileId = p.Id,  
13.         TimeZoneSidKey = 'America/Los_Angeles',  
14.         UserName = uniqueUserName);  
15.   
16.         System.runAs(u) {  
17.           
18.               // The following code runs as user 'u'  
19.               System.debug('Current User: ' + UserInfo.getUserName());  
20.               System.debug('Current Profile: ' + UserInfo.getProfileId());  
21.                 
22.         }  
23.     }  
24.       
25. }  

6. Write comments stating not only what is supposed to be tested, but the assumptions the tester made about the data, the expected outcome, and so on.

7. Test the classes in your application individually. Never test your entire application in a single test.

8. Use Mock responses for testing callouts. Check Appendix B and C for sample test classes.

9. Avoid using seeAllData=true.

10. Avoid hard coding Ids, etc.

12. Adding SOSL Queries to Unit Tests 

To ensure that test methods always behave in a predictable way, any Salesforce Object Search Language (SOSL) query that is added to an Apex test method returns an empty set of search results when the test method executes. If you do not want the query to return an empty list of results, you can use the Test.setFixedSearchResults system method to define a list of record IDs that are returned by the search. All SOSL queries that take place later in the test method return the list of record IDs that were specified by the Test.setFixedSearchResults method. Additionally, the test method can call Test.setFixedSearchResults multiple times to define different result sets for different SOSL queries. If you do not call the Test.setFixedSearchResults method in a test method, or if you call this method without specifying a list of record IDs, any SOSL queries that take place later in the test method return an empty list of results.

The list of record IDs specified by the Test.setFixedSearchResults method replaces the results that would normally be returned by the SOSL query if it were not subject to any WHERE or LIMIT clauses. If these clauses exist in the SOSL query, they are applied to the list of fixed search results.

Note:
1. Although the record may not match the query string in the FIND clause, the record is passed into the RETURNING clause of the SOSL statement.
2. If the record matches the WHERE clause filter, the record is returned. If it does not match the WHERE clause, no record is returned.

Sample code:

Apex Controller:


  1. public class SOSLController {  
  2.   
  3.     public static List < List < SObject > > searchAccountContactLead( String strSearch ) {  
  4.       
  5.         String searchQuery = 'FIND \'' + strSearch + '*\' IN ALL FIELDS RETURNING Account( Id, Name WHERE Industry = \'Apparel\' ), Contact, Lead';   
  6.         return search.query( searchQuery );  
  7.       
  8.     }  
  9.       
  10. }  

Test class:


  1. @isTest  
  2. private class SOSLControllerTest {  
  3.   
  4.     static testMethod void testSOSL() {  
  5.       
  6.         Account acc = new Account( Name = 'Testing', Industry = 'Banking' );  
  7.         insert acc;  
  8.         List < List < SObject > > searchResults = SOSLController.searchAccountContactLead( 'Sample' );  
  9.         List < Account > listAccount = searchResults.get( 0 );  
  10.         system.assertEquals( 0, listAccount.size() );//Size will be zero since setFixedSearchResults is not set  
  11.         Id [] fixedSearchResults = new Id[1];  
  12.         fixedSearchResults[0] = acc.Id;  
  13.         Test.setFixedSearchResults( fixedSearchResults );  
  14.         searchResults = SOSLController.searchAccountContactLead( 'Sample' );  
  15.         listAccount = searchResults.get( 0 );  
  16.         system.assertEquals( 0, ( (List<Account>)searchResults.get( 0 ) ).size() );//Size will be still zero since WHERE condition fails for account  
  17.         acc.Industry = 'Apparel';  
  18.         update acc;  
  19.         searchResults = SOSLController.searchAccountContactLead( 'Sample' );  
  20.         listAccount = searchResults.get( 0 );  
  21.         system.assertEquals( 1, listAccount.size() );//Size will be one since Account WHERE conditions succeeds  
  22.       
  23.     }  
  24.       
  25. }  

Appendix - A - Sample Test Class for Schedulable_Class

1.   @istest  
2.   public with sharing class SampleTest {  
3.     
4.       static testmethod void testSample() {  
5.         
6.           Test.startTest();  
7.           Schedulable_Class  obj = new Schedulable_Class();  
8.           obj.execute(null);  
9.           Test.stopTest();  
10.           
11.     }  
12.   
13. }  

Appendix - B - Test class for HTTPCallouts in Salesforce

Sample HTTPCallout Class:

1.   public class AnimalLocator {  
2.     
3.       public static String getAnimalNameById(Integer id) {  
4.         
5.           Http http = new Http();  
6.           HttpRequest request = new HttpRequest();  
7.           request.setEndpoint('https://th-apex-http-callout.herokuapp.com/animals/'+id);  
8.           request.setMethod('GET');  
9.           HttpResponse response = http.send(request);  
10.         String strResp = '';  
11.         if (response.getStatusCode() == 200) {  
12.           
13.            Map < String, Object > results = (Map < String, Object >) JSON.deserializeUntyped(response.getBody());  
14.            Map < string, Object > animals = (Map < String, Object >) results.get('animal');  
15.            strResp = string.valueof(animals.get('name'));  
16.              
17.         }  
18.         return strResp;  
19.           
20.     }    
21. }  

Sample HTTP Mock Callout Class:

1.   @isTest  
2.   global class AnimalLocatorMock implements HttpCalloutMock {  
3.     
4.       global HTTPResponse respond(HTTPRequest request) {  
5.         
6.           HttpResponse response = new HttpResponse();  
7.           response.setHeader('Content-Type''application/json');  
8.           response.setBody('{"animal": {"id":2, "name":"Test"}}');  
9.           response.setStatusCode(200);  
10.         return response;   
11.           
12.     }  
13.       
14. }  

Test Class:

1.   @isTest   
2.   private class AnimalLocatorTest {  
3.     
4.       static testMethod void testPostCallout() {  
5.         
6.           Test.setMock(HttpCalloutMock.classnew AnimalLocatorMock());    
7.           String strResp = AnimalLocator.getAnimalNameById(2);  
8.             
9.       }  
10. }  

Appendix - C - Test class for SOAP Callout in Salesforce

Class generated from WSDL:

1.   //Generated by wsdl2apex  
2.     
3.   public class ParkService {  
4.     
5.       public class byCountryResponse {  
6.         
7.           public String[] return_x;  
8.           private String[] return_x_type_info = new String[]{'return','http://parks.services/',null,'0','-1','false'};  
9.           private String[] apex_schema_type_info = new String[]{'http://parks.services/','false','false'};  
10.         private String[] field_order_type_info = new String[]{'return_x'};  
11.           
12.     }  
13.     public class byCountry {  
14.       
15.         public String arg0;  
16.         private String[] arg0_type_info = new String[]{'arg0','http://parks.services/',null,'0','1','false'};  
17.         private String[] apex_schema_type_info = new String[]{'http://parks.services/','false','false'};  
18.         private String[] field_order_type_info = new String[]{'arg0'};  
19.           
20.     }  
21.     public class ParksImplPort {  
22.       
23.         public String endpoint_x = 'https://th-apex-soap-service.herokuapp.com/service/parks';  
24.         public Map<String,String> inputHttpHeaders_x;  
25.         public Map<String,String> outputHttpHeaders_x;  
26.         public String clientCertName_x;  
27.         public String clientCert_x;  
28.         public String clientCertPasswd_x;  
29.         public Integer timeout_x;  
30.         private String[] ns_map_type_info = new String[]{'http://parks.services/''ParkService'};  
31.         public String[] byCountry(String arg0) {  
32.           
33.             ParkService.byCountry request_x = new ParkService.byCountry();  
34.             request_x.arg0 = arg0;  
35.             ParkService.byCountryResponse response_x;  
36.             Map<String, ParkService.byCountryResponse> response_map_x = new Map<String, ParkService.byCountryResponse>();  
37.             response_map_x.put('response_x', response_x);  
38.             WebServiceCallout.invoke(  
39.               this,  
40.               request_x,  
41.               response_map_x,  
42.               new String[]{endpoint_x,  
43.               '',  
44.               'http://parks.services/',  
45.               'byCountry',  
46.               'http://parks.services/',  
47.               'byCountryResponse',  
48.               'ParkService.byCountryResponse'}  
49.             );  
50.             response_x = response_map_x.get('response_x');  
51.             return response_x.return_x;  
52.               
53.         }  
54.           
55.     }  
56.       
57. }  

Class making use of WSDL generated Class:

1.   public class ParkLocator {  
2.     
3.       public static List < String > country(String Country) {  
4.         
5.           ParkService.ParksImplPort obj =   
6.               new ParkService.ParksImplPort();  
7.           return obj.byCountry(Country);  
8.             
9.       }  
10.       
11. }  

Mock for test class:

1.   @isTest  
2.   global class ParkServiceMock implements WebServiceMock {  
3.     
4.      global void doInvoke(  
5.              Object stub,  
6.              Object request,  
7.              Map<String, Object> response,  
8.              String endpoint,  
9.              String soapAction,  
10.            String requestName,  
11.            String responseNS,  
12.            String responseName,  
13.            String responseType) {  
14.              
15.         ParkService.byCountryResponse response_x = new ParkService.byCountryResponse();  
16.         response_x.return_x = new List < String > {'a''b'};  
17.         response.put('response_x', response_x);   
18.           
19.    }  
20.      
21. }  

Test Class:

1.   @isTest  
2.   private class ParkLocatorTest {  
3.     
4.       @isTest static void testCallout() {        
5.         
6.           Test.setMock(WebServiceMock.classnew ParkServiceMock());  
7.           List < String > result = ParkLocator.country('Test');  
8.             
9.       }  
10.       
11. }  

Appendix - D - Test class for Email Services classes in Apex
In the test class, follow the below

1. Create Messaging.InboundEmail.

2. Create Messaging.InboundEnvelope.

3. Pass them to handleInboundEmail() method of Messaging.InboundEmailHandler class.

Sample Test Class Code:

1.   Messaging.InboundEmail email = new Messaging.InboundEmail() ;  
2.   Messaging.InboundEnvelope env = new Messaging.InboundEnvelope();  
3.     
4.   email.subject = 'Test';  
5.   email.fromname = 'Test Test';  
6.   env.fromAddress = 'Test@email.com';  
7.   email.plainTextBody = 'Test';  
8.     
9.   CreateLeadInboundHandler emailProcess = new CreateLeadInboundHandler();  
10. emailProcess.handleInboundEmail(email, env);  

here CreateLeadInboundHandler is a Email Service Apex class.

Appendix - E - Test code coverage for private methods in Apex
TestVisible annotation allow test methods to access private or protected members of another class outside the test class. These members include methods, member variables, and inner classes.

Sample Class:

1.   public class TestVisibleExample {  
2.     
3.       // Private member variable  
4.       @TestVisible private static Integer recordNumber = 1;  
5.     
6.       // Private method  
7.       @TestVisible private static void updateRec() {  
8.       }  
9.     
10. }    

Test Class:

1.   @isTest  
2.   private class TestVisibleExampleTest {  
3.       @isTest static void test1() {  
4.           // Accessing private variable annotated with TestVisible  
5.           Integer i = TestVisibleExample.recordNumber;  
6.           System.assertEquals(1, i);  
7.     
8.           // Accessing private method annotated with TestVisible  
9.           TestVisibleExample.updateRecord();  
10.     }  
11. }  

References:



No comments:

Post a Comment