Test Classes & Testing Strategy

0/9 in this phase0/41 across the roadmap

📖 Concept

Salesforce requires a minimum of 75% code coverage to deploy to production. But at the senior level, testing strategy goes far beyond coverage percentages — it's about verifying business logic, testing edge cases, and ensuring production reliability.

Testing in Salesforce — key concepts:

  1. @isTest annotation — Marks a class or method as a test
  2. Test.startTest() / Test.stopTest() — Resets governor limits for the code under test; also executes async code synchronously
  3. System.assert / assertEquals / assertNotEquals — Verify expected outcomes
  4. @TestSetup — Create test data once, available to all test methods in the class
  5. System.runAs() — Execute code as a specific user (test security)
  6. Test.getStandardPricebookId() — Access standard pricebook in tests
  7. HttpCalloutMock / WebServiceMock — Mock external callouts

Test data strategy:

  • NEVER rely on existing org data — tests must create their own data
  • Use @TestSetup for shared test data across methods
  • Build a TestDataFactory class for reusable record creation
  • Use SeeAllData=false (default) — tests don't see org data
  • Only use SeeAllData=true for specific edge cases (like testing with standard pricebooks)

What to test:

  1. Positive tests — Does the feature work correctly with valid input?
  2. Negative tests — Does it handle invalid input gracefully?
  3. Bulk tests — Does it work with 200 records (trigger bulk)?
  4. Security tests — Does it respect sharing and FLS?
  5. Edge cases — Null values, empty lists, boundary conditions
  6. Permissions tests — Does it work for different user profiles?

Beyond 75% coverage: At the senior level, coverage percentage is not the goal — behavioral verification is. A test that inserts a record and asserts nothing is worthless even if it provides coverage. Every test should have meaningful assertions that verify business logic.

💻 Code Example

codeTap to expand ⛶
1// Testing Strategy — Enterprise Patterns
2
3// 1. TestDataFactory — Reusable test data creation
4@isTest
5public class TestDataFactory {
6
7 public static Account createAccount(String name) {
8 return new Account(
9 Name = name,
10 Industry = 'Technology',
11 BillingCity = 'San Francisco',
12 BillingState = 'CA'
13 );
14 }
15
16 public static List<Account> createAccounts(Integer count) {
17 List<Account> accounts = new List<Account>();
18 for (Integer i = 0; i < count; i++) {
19 accounts.add(createAccount('Test Account ' + i));
20 }
21 return accounts;
22 }
23
24 public static Contact createContact(Id accountId) {
25 return new Contact(
26 FirstName = 'Test',
27 LastName = 'Contact',
28 AccountId = accountId,
29 Email = 'test' + Crypto.getRandomInteger() + '@example.com'
30 );
31 }
32
33 public static Opportunity createOpportunity(Id accountId, String stage) {
34 return new Opportunity(
35 Name = 'Test Opportunity',
36 AccountId = accountId,
37 StageName = stage,
38 CloseDate = Date.today().addDays(30),
39 Amount = 100000
40 );
41 }
42
43 public static User createUser(String profileName) {
44 Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
45 String uniqueKey = EncodingUtil.convertToHex(
46 Crypto.generateAESKey(128)
47 ).substring(0, 8);
48
49 return new User(
50 FirstName = 'Test',
51 LastName = 'User ' + uniqueKey,
52 Email = uniqueKey + '@test.com',
53 Username = uniqueKey + '@test.com.sandbox',
54 ProfileId = p.Id,
55 Alias = uniqueKey.substring(0, 5),
56 TimeZoneSidKey = 'America/Los_Angeles',
57 LocaleSidKey = 'en_US',
58 EmailEncodingKey = 'UTF-8',
59 LanguageLocaleKey = 'en_US'
60 );
61 }
62}
63
64// 2. Comprehensive Test Class Example
65@isTest
66private class OpportunityServiceTest {
67
68 @TestSetup
69 static void setupTestData() {
70 // Create shared test data — runs once for all test methods
71 Account acc = TestDataFactory.createAccount('Test Corp');
72 insert acc;
73
74 List<Opportunity> opps = new List<Opportunity>();
75 for (Integer i = 0; i < 5; i++) {
76 opps.add(TestDataFactory.createOpportunity(acc.Id, 'Prospecting'));
77 }
78 insert opps;
79 }
80
81 // Positive test — normal flow
82 @isTest
83 static void testSetDefaults_setsCorrectValues() {
84 List<Opportunity> opps = new List<Opportunity>{
85 new Opportunity(Name = 'Test', CloseDate = Date.today())
86 };
87
88 Test.startTest();
89 OpportunityService.setDefaults(opps);
90 Test.stopTest();
91
92 System.assertEquals('Prospecting', opps[0].StageName,
93 'Default stage should be Prospecting');
94 System.assertEquals(10, opps[0].Probability,
95 'Default probability should be 10');
96 }
97
98 // Negative test — validation
99 @isTest
100 static void testValidateAmounts_rejectsNegative() {
101 Account acc = [SELECT Id FROM Account LIMIT 1];
102 Opportunity opp = TestDataFactory.createOpportunity(acc.Id, 'Prospecting');
103 opp.Amount = -5000;
104
105 Test.startTest();
106 Database.SaveResult sr = Database.insert(opp, false);
107 Test.stopTest();
108
109 System.assert(!sr.isSuccess(), 'Negative amount should be rejected');
110 }
111
112 // Bulk test — 200 records
113 @isTest
114 static void testBulkInsert_handlesMany() {
115 Account acc = [SELECT Id FROM Account LIMIT 1];
116 List<Opportunity> opps = new List<Opportunity>();
117 for (Integer i = 0; i < 200; i++) {
118 opps.add(TestDataFactory.createOpportunity(acc.Id, 'Prospecting'));
119 }
120
121 Test.startTest();
122 insert opps; // Should trigger without governor limit exceptions
123 Test.stopTest();
124
125 System.assertEquals(200, [
126 SELECT COUNT() FROM Opportunity
127 WHERE AccountId = :acc.Id AND StageName = 'Prospecting'
128 ] - 5, 'All 200 records should be inserted'); // -5 from @TestSetup
129 }
130
131 // Security test — different user profile
132 @isTest
133 static void testWithStandardUser_respectsSharing() {
134 User standardUser = TestDataFactory.createUser('Standard User');
135 insert standardUser;
136
137 System.runAs(standardUser) {
138 Test.startTest();
139 List<Account> visible = [SELECT Id FROM Account];
140 Test.stopTest();
141
142 // Standard user may not see all accounts depending on OWD
143 System.assertNotEquals(null, visible,
144 'Query should execute without errors');
145 }
146 }
147
148 // Mock callout test
149 @isTest
150 static void testExternalSync_handlesResponse() {
151 Test.setMock(HttpCalloutMock.class, new MockHttpResponse());
152
153 Test.startTest();
154 // Call the method that makes a callout
155 FutureExample.syncToExternalSystem(
156 new Set<Id>{[SELECT Id FROM Account LIMIT 1].Id}
157 );
158 Test.stopTest();
159
160 // Verify the callout was processed
161 System.assert(true, 'Callout completed without errors');
162 }
163}
164
165// 3. HTTP Callout Mock
166@isTest
167public class MockHttpResponse implements HttpCalloutMock {
168 public HTTPResponse respond(HTTPRequest req) {
169 HttpResponse res = new HttpResponse();
170 res.setHeader('Content-Type', 'application/json');
171 res.setBody('{"status":"success","id":"ext-123"}');
172 res.setStatusCode(200);
173 return res;
174 }
175}
176
177// 4. Testing Platform Events
178@isTest
179private class OrderEventTest {
180
181 @isTest
182 static void testEventPublish() {
183 Order_Event__e event = new Order_Event__e(
184 Order_Id__c = '001000000000001',
185 Total_Amount__c = 75000,
186 Event_Type__c = 'ORDER_CREATED'
187 );
188
189 Test.startTest();
190 Database.SaveResult sr = EventBus.publish(event);
191 Test.stopTest();
192
193 System.assert(sr.isSuccess(), 'Event should publish successfully');
194 }
195
196 @isTest
197 static void testEventSubscriber() {
198 // Deliver events in test context
199 Order_Event__e event = new Order_Event__e(
200 Order_Id__c = '001000000000001',
201 Total_Amount__c = 75000,
202 Event_Type__c = 'ORDER_CREATED'
203 );
204
205 Test.startTest();
206 EventBus.publish(event);
207 Test.getEventBus().deliver(); // Force delivery in test
208 Test.stopTest();
209
210 // Verify subscriber created tasks
211 List<Task> tasks = [SELECT Subject FROM Task WHERE Subject LIKE '%Large order%'];
212 System.assert(tasks.size() > 0, 'Subscriber should create task for large orders');
213 }
214}

🏋️ Practice Exercise

Testing Practice:

  1. Create a TestDataFactory with methods for Account, Contact, Opportunity, Case, and User
  2. Write a test class with @TestSetup that creates shared data for 5 test methods
  3. Write a bulk test that inserts 200 records and verifies trigger behavior
  4. Implement a mock HTTP callout and test a class that makes external API calls
  5. Write negative test cases that verify exceptions are thrown for invalid input
  6. Test a sharing scenario using System.runAs() with Standard User and Admin profiles
  7. Write a test for a Batch Apex class — verify start(), execute(), and finish()
  8. Test Platform Event publishing and subscriber behavior using Test.getEventBus().deliver()
  9. Achieve 95%+ coverage for a complex trigger handler with all 7 trigger events
  10. Write a test that verifies field-level security using Security.stripInaccessible()

⚠️ Common Mistakes

  • Writing tests that only increase coverage without assertions — empty tests pass but don't verify anything

  • Using SeeAllData=true when not needed — this makes tests dependent on org data and causes failures in different environments

  • Not testing bulk scenarios (200 records) — code that works for 1 record may hit governor limits with 200

  • Forgetting Test.startTest() and Test.stopTest() — async code won't execute without these, and governor limits won't reset

  • Hard-coding record IDs in tests — IDs are different across orgs. Always create test data dynamically

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Test Classes & Testing Strategy. Login to unlock this feature.