2. Agenda
● Welcome/Intro
● Today’s Topics:
○ Closing the Test Automation Gap: A Guide to Increasing Your Velocity
■ Michael Dailey
○ The Return of H.O.T. in Cleveland
■ Lynda Kane
● Announcements & Upcoming Local Events
15. Lynda’s “Safe Harbor” Statement
The topic presented in today’s presentation on Apex testing tends to be boring and
dry at best. In an effort to draw your attention and provide some comic relief
liberties have been taken. The portrayal and ideas presented here are my own
and in no way a reflection on Salesforce, the Trailblazer Community, the
Salesforce Ohaha, this Trailblazer Community group, or Developers in General. If
you are offended by or feel some of this presentation is blasphemous to your
religious beliefs, I apologize as I mean no disrespect. I only hope to provide some
entertainment while drawing your attention to and assisting you in better
understanding and being comfortable with this important topic.
17. Faced with this Commandment...
We struggle, we worry, we freak out.
How do we do this?
We search out and “borrow” code to get the functionality
we need but those examples almost never include test
class. We create our own code and test classes. We
ponder and stress over creating unit tests to get coverage
for every line.
And then...
18. API Version 24.0
Like a new Pharaoh in Egypt, starting with
API v24.0 we could no longer access all
the data in our orgs by default when
creating test classes.
So now we’re not only worried and
sweating, we’re feeling shackled and
oppressed not unlike the slaves in the
oppressively HOT Egyptian desert...
19. We need an oasis.
We need a plan.
We need a savior.
Like the Egyptian slaves, we need a
spokesperson.
We need...
21. Unfortunately she wasn’t available so...
You get me.
● 13+ Years Salesforce
Experience
● 11X Certified
● 1800+ Trailhead Badges
But don’t worry I brought
2 tablets...
22. I. Thou shall keep code
current.
II. Thou shall use
original test data.
III. Thou shall honor the
Governor Limits.
IV. Thou shall include all
inputs.
V. Thou shall bulkify.
VI. Thou shall test multiple
contexts.
VII. Thou shall perform
simulations.
VIII. Thou shall confirm
expectations &
exceptions.
IX. Thou shall order results.
X. Thou shall comment.
23. … and tools
We need to take advantage of software development
methodologies like TDD (Test-Driven Development), BDD
(Behavior Driven Development), MBT (Model Based Testing),
etc. so that we grow into better developers who focus not on
75% coverage but on making sure we’re doing the right
things in our code and testing those thoroughly.
24. Lynda’s Golden Rule
Focus not on covering every
line but on testing the expected
functionality.
Think and create tests that:
● Portray initial data as the
code will receive it
● Assert the code affects that
data as expected
Test as you expect
the code to treat
your data.
25. Back to our tale
We have arrived at the
promised land, the land of Milk
& Honey (or maybe Milk &
Cookies when we’re good
developers), a land filled with
successful deployments, high
(even higher than 75%) code
coverage, proven tests and
happy Salesforce Admins and
Users. Hallelujah!
27. Thou shall keep code current.
Salesforce Recommends: No more than 3 API Versions in code
Lynda Recommends: Use the most recent API Versions you are able to (& limit
the # of Versions)
As of June 2023, API 30.0 and earlier will be retired.
Test code using API Version 23.0 & earlier had access to all data by default
(SeeAllData = true has no effect here). Test code with version 24.0 and later does
not. If a test class with version 24.0 or later uses code with version 23.0 or earlier,
you still can’t access all data by default
Sometimes APEX commands are retired and newer API’s have great new features
for controlling security concerns.
28. Thou shall use original test data.
Salesforce Recommends: Create the necessary data in test classes, so the tests
do not have to rely on data in a particular organization. Create all test data before
calling the Test.startTest method. Since tests don't commit, you don't have to
delete any data.
Lynda Recommends: Create test data that mimics the behavior of your users
How:
● Create in each test method
● Use @TestSetup to create test data for a class and its methods
● Use a TestFactory class to generate test data and call it in @TestSetup and/or
individual test methods
● Store Test Data in a .CSV file in Static Resources, Use List<sObject> ls
= Test.loadData(Account.sObjectType, 'myResource');
29. Thou shall honor the Governor Limits.
Lynda Recommends: Use Test.startTest() and Test.stopTest()
Notes:
● Does not reset the context but provides an additional context
● While in this context, Governor Limits are separate
● Can only use Once (1) per Test Method!
● After stopTest(), return to original context and usage against original Governor
Limit levels continues.
● With startTest(), asynchronous calls are collected; At stopTest(), all
asynchronous processes are executed
30. Thou shall include all inputs.
Salesforce Recommends: If code uses conditional logic (including ternary
operators), execute each branch. Make calls to methods using both valid and
invalid inputs.
Lynda Recommends: Plan ahead and outline all the possible input data (strive
for at least 85% of anticipated situations)
31. Thou shall bulkify.
Salesforce Recommends: Exercise bulk trigger functionality—use at least 20
records in your tests.
Lynda Recommends:
● Tests with multiple records.
● Remember: IDs may not be in order
● Assert more than 1 resulting record.
32. Thou shall test with multiple contexts.
Salesforce Recommends: Use the runAs method to test your application in
different user contexts.
Lynda Recommends:
● Make sure user profiles/permissions can’t change data when they shouldn’t.
● Remember: System Administrators have Modify All Data.
● Keep in mind the Sharing Settings
33. Thou shall perform simulations.
Lynda Recommends: Use Mocks and Stubs where they make sense
● REST/SOAP Calls to External Systems
● REST/SOAP Calls coming to Salesforce
● Other external data usage
● Web site responses
● Simulating email sends
Note: This is an advanced developer topic!
Learn more:
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_stub_api.htm
34. Thou shall confirm expectations & exceptions.
Salesforce Recommends: Use the Assert class to prove that code behaves
properly. Complete successfully without throwing any exceptions, unless those
errors are expected and caught in a try…catch block. Always handle all exceptions
that are caught, instead of merely catching the exceptions.
Lynda Recommends:
● Strive for a minimum of 2 Asserts per Test Method.
● Assert if you import data (from a Static Resource) or generate bulk data
before you run that data through tests.
● If you use try...catch, be sure to create methods that raise the exceptions.
● If you used System.Assert, learn about the new Assert class.
Learn more:
https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_class_System_Assert.htm#
apex_class_System_Assert
35. Thou shall order results.
Salesforce Recommends: Not assume that record IDs are in sequential order.
Record IDs are not created in ascending order unless you insert multiple records
with the same request. For example, if you create an account A, and receive the
ID 001D000000IEEmT, then create account B, the ID of account B may, or may
not be sequentially higher.
Lynda Recommends:
● User ORDER BY in your SOQL when retrieving data post-test, especially if
you are using LIMIT
● ORDER BY known fields not IDs.
36. Thou shall comment.
Salesforce Recommends: 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.
Lynda Recommends:
● Help your future self, other developers and your admin!
● Include a documentation box after the first line (class declaration)
● Include who created & when, what methods are included with short
description, date/who/what for updates.
● For classes, identify the primary test class in the documentation box.
● For test classes, provide what you’re trying to test.
● Comment assumptions where used in the code.
37. Documentation Box Example: Apex Class
public class UserUtility {
/* Utilities for use with User object
* Created 6/30/2019 Lynda Kane
*
* Methods:
* getAllSubordinateIds - takes set of User.ID's and returns Set of UserID's for all
subordinate User.Manager
* getAllManagerIds - takes set of UserID's and returns Set of of UserID's for all managers
User.Manager
*
* Update Log:
*
* Test Class: see UserUtilityTest
*/
public static Set<ID> getAllSubordinateIds(Set<ID> EmpIds) {
// To get all sub employees (User.Manager field)
Set<ID> currentEmpIds = new Set<ID>();
...
38. Documentation Box Example: Apex Test Class
@isTest
public class UserUtilityTest {
/* Test Class for Utilities for use with User object
* Created 6/30/2019 Lynda Kane
*
* Methods:
* testSubordinatesGet - creates 1 manager and 2 user they manage, tests getAllSubordinateIds
(Happy Path)
* testManagersGet - creates 3 users in a 3-level hierarchy, tests getAllManagersIds (Happy
Path)
*
* Update Log:
*
*/
static testMethod void testSubordinatesGet() {
39. Running Unit Tests
Lynda Recommends: Run your tests often (while developing and in a preview
sandbox for each quarterly release once in production)
How:
● Salesforce User Interface (Setup - Apex Classes or Apex Text Execution)
● Developer Console
● Force.com IDE (Eclipse) - note Eclipse is no longer supported
● SFDX via the Salesforce CLI
● SOAP API
○ RunTestsResult[] runTests(RunTestsRequest ri)
40. Learn Moar About Testing
Apex Developers Guide:
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex
_testing.htm
Trailhead Modules on Testing:
https://trailhead.salesforce.com/en/content/learn/modules/apex_testing
https://trailhead.salesforce.com/en/content/learn/modules/unit-testing-on-the-lightn
ing-platform
About Software Development Methodologies:
https://www.browserstack.com/guide/tdd-vs-bdd-vs-atdd
42. Test Class Writing Practice
For each Apex Class Presented, our goals are to:
● Add a documentation box and comments
● Create a test class that tests the functionality
● Hopefully achieve >= 75% code coverage (but
functionality is our focus)
● Utilize best practices
43. Example 1
public class OlderAccountsUtility {
public static void updateOlderAccounts() {
// Get the 5 oldest accounts
Account[] oldAccounts = [SELECT Id, Description FROM Account ORDER
BY CreatedDate ASC LIMIT 5];
// loop through them and update the Description field
for (Account acct : oldAccounts) {
acct.Description = 'Heritage Account';
}
// save the change you made
update oldAccounts;
}
}
Source: https://trailhead.salesforce.com/content/learn/projects/quickstart-apex
44. Example 1: Our Modified Class
public class OlderAccountsUtility {
/*Created lbk 7/13/2019
*
* updateOlderAccount - updates description for 5 oldest accounts
*
* update log:
* 7/13/2019 lbk Changed to use Legacy_Status__c field instead
*
* test class: see OlderAccountsUtilityTest
*
* */
public static void updateOlderAccounts() {
// Get the 5 oldest accounts
Account[] oldAccounts = [SELECT Id, Legacy_Status__c FROM Account ORDER BY CreatedDate ASC LIMIT 5];
// loop through them and update the Description field
for (Account acct : oldAccounts) {
acct.Legacy_Status__c = 'Heritage';
}
// save the change you made
update oldAccounts;
}
}
45. Example 1: Our Test Class
@isTest
public class OlderAccountsUtilityTest {
/*Created by lbk 7/13/2019
*
* testSetupAccounts - create 10 account records
* testUpdateAccounts - runs the test and checks the changed and unchanged records
*
* update log:
*
*/
@testSetup static void testSetupAccounts() {
List<Account> createAccounts = new List<Account>();
for (Integer i = 0; i < 10; i++) {
Account a = new Account();
a.name = 'Test Account ' + i;
createAccounts.add(a);
}
insert createAccounts;
}
46. Example 1: Our Test Class cont.
@isTest static void testUpdateAccounts() {
List<Account> updateAccounts = [SELECT Id, Legacy_Status__c, Name FROM Account];
Assert.areEqual(10, updateAccounts.size());
Test.startTest();
OlderAccountsUtility.updateOlderAccounts();
Test.stopTest();
List<Account> check1 = [SELECT Id FROM Account WHERE Legacy_Status__c = 'Heritage'];
List<Account> check2 = [SELECT Id FROM Account WHERE Legacy_Status__c = 'New'];
Assert.areEqual(5, check1.size());
Assert.areEqual(5, check2.size());
}
}
47. Example 2
public with sharing class AccountHandler {
public static void CreateNewOpportunity(List<Account> accts) {
for (Account a : accts) {
Opportunity opp = new Opportunity();
opp.Name = a.Name + ' Opportunity';
opp.AccountId = a.Id;
opp.StageName = 'Prospecting';
opp.CloseDate = System.Today().addMonths(1);
insert opp;
}
}
}
Source: https://trailhead.salesforce.com/content/learn/modules/apex_basics_dotnet/execution_context
48. Example 2: Our Modified Class
public with sharing class AccountHandler {
/*Created lbk 7/13/2019
*
* CreateNewOpportunity: for a list of accounts, add an opportunity with
a Close Date 1 month out
*
* update log:
*
* test class: see AccountHandlerTest
*/
public static void CreateNewOpportunity(List<Account> accts) {
...
49. Example 2: Our Test Class
@isTest
public class AccountHandlerTest {
/* created lbk 7/13/2019
*
* update log:
*
* testNewAccountCreate - checks that new Opps created with new Accounts
*/
@isTest static void testNewAccountCreate() {
List<Account> createAccounts = new List<Account>();
for (Integer i = 0; i < 7; i++) {
Account a = new Account();
a.Name = 'Test Account ' + i;
createAccounts.add(a);
}
50. Example 2: Our Test Class cont.
insert createAccounts;
List<Account> runAccounts = [SELECT Id, Name FROM Account];
Assert.areEqual(7, runAccounts.size());
Test.startTest();
AccountHandler.CreateNewOpportunity(runAccounts);
Test.stopTest();
List<Opportunity> findOpps = [SELECT ID, Name, StageName, CloseDate FROM Opportunity ORDER BY Name];
system.assertEquals(7, findOpps.size());
Assert.areEqual('Prospecting', findOpps[2].StageName);
Assert.areEqual(System.Today().addMonths(1), findOpps[4].CloseDate);
}
}
51. Example 3
trigger ClosedOppTrigger on Opportunity (after insert,after update) {
List<task> carry=New List<task>();
for(opportunity opp:trigger.new){
if(opp.stagename=='Closed Won'){
task t=new task(whatid=opp.id,Status = 'Active',Subject = 'Follow Up
Test Task',ActivityDate = system.today() );
carry.add(t);
}
}
insert carry;
}
Source: https://developer.salesforce.com/forums/?id=906F00000005FGhIAM
52. Example 3: Our Modified Trigger
trigger ClosedOppTrigger on Opportunity (after insert,after update) {
/* created 7/13/2019 lbk
*
* add task to won opportunity to do followup
*
* update log:
*
* tests class: ClosedOppTriggerTest
*/
List<task> carry=New List<task>();
...
53. Example 3: Our Test Class
@isTest
public class ClosedOppTriggerTest {
/* created lbk 7/13/2019
*
* testSetupOpps - create 10 opps for testing
* testWonOpps - move 3 opps to Closed won and make sure Tasks were created
*
* update log:
*
*/
@testSetup static void testSetupOpps() {
54. Example 3: Our Test Class
@testSetup static void testSetupOpps() {
Account a = new Account(Name = 'Test Account');
insert a;
List<Opportunity> createOpps = new List<Opportunity>();
for (Integer i = 0; i < 10; i++) {
Opportunity opp = new Opportunity();
opp.Name = a.Name + ' ' + i;
opp.AccountId = a.Id;
opp.CloseDate = System.TODAY() + 90;
opp.StageName = 'Prospecting';
createOpps.add(opp);
}
insert createOpps;
}
55. Example 3: Our Test Class
@isTest static void testWonOpps() {
List<Opportunity> updateOpps = [SELECT Id, Name, StageName FROM Opportunity ORDER BY Name LIMIT 3];
System.Debug(updateOpps);
Test.startTest();
for (Opportunity o : updateOpps) {
o.StageName = 'Closed Won';
}
update updateOpps;
Test.stopTest();
List<Task> getTasks = [SELECT Id, Subject, ActivityDate, Status, WhatId FROM Task ORDER BY WhatID];
updateOpps.sort();
Assert.areEqual(3, getTasks.size());
Assert.areEqual(updateOpps[2].Id, getTasks[2].WhatId);
Assert.areEqual('Active',getTasks[1].Status);
}
}