Test Classes & Testing Strategy
📖 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:
- @isTest annotation — Marks a class or method as a test
- Test.startTest() / Test.stopTest() — Resets governor limits for the code under test; also executes async code synchronously
- System.assert / assertEquals / assertNotEquals — Verify expected outcomes
- @TestSetup — Create test data once, available to all test methods in the class
- System.runAs() — Execute code as a specific user (test security)
- Test.getStandardPricebookId() — Access standard pricebook in tests
- 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=truefor specific edge cases (like testing with standard pricebooks)
What to test:
- Positive tests — Does the feature work correctly with valid input?
- Negative tests — Does it handle invalid input gracefully?
- Bulk tests — Does it work with 200 records (trigger bulk)?
- Security tests — Does it respect sharing and FLS?
- Edge cases — Null values, empty lists, boundary conditions
- 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
1// Testing Strategy — Enterprise Patterns23// 1. TestDataFactory — Reusable test data creation4@isTest5public class TestDataFactory {67 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 }1516 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 }2324 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 }3233 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 = 10000040 );41 }4243 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);4849 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}6364// 2. Comprehensive Test Class Example65@isTest66private class OpportunityServiceTest {6768 @TestSetup69 static void setupTestData() {70 // Create shared test data — runs once for all test methods71 Account acc = TestDataFactory.createAccount('Test Corp');72 insert acc;7374 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 }8081 // Positive test — normal flow82 @isTest83 static void testSetDefaults_setsCorrectValues() {84 List<Opportunity> opps = new List<Opportunity>{85 new Opportunity(Name = 'Test', CloseDate = Date.today())86 };8788 Test.startTest();89 OpportunityService.setDefaults(opps);90 Test.stopTest();9192 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 }9798 // Negative test — validation99 @isTest100 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;104105 Test.startTest();106 Database.SaveResult sr = Database.insert(opp, false);107 Test.stopTest();108109 System.assert(!sr.isSuccess(), 'Negative amount should be rejected');110 }111112 // Bulk test — 200 records113 @isTest114 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 }120121 Test.startTest();122 insert opps; // Should trigger without governor limit exceptions123 Test.stopTest();124125 System.assertEquals(200, [126 SELECT COUNT() FROM Opportunity127 WHERE AccountId = :acc.Id AND StageName = 'Prospecting'128 ] - 5, 'All 200 records should be inserted'); // -5 from @TestSetup129 }130131 // Security test — different user profile132 @isTest133 static void testWithStandardUser_respectsSharing() {134 User standardUser = TestDataFactory.createUser('Standard User');135 insert standardUser;136137 System.runAs(standardUser) {138 Test.startTest();139 List<Account> visible = [SELECT Id FROM Account];140 Test.stopTest();141142 // Standard user may not see all accounts depending on OWD143 System.assertNotEquals(null, visible,144 'Query should execute without errors');145 }146 }147148 // Mock callout test149 @isTest150 static void testExternalSync_handlesResponse() {151 Test.setMock(HttpCalloutMock.class, new MockHttpResponse());152153 Test.startTest();154 // Call the method that makes a callout155 FutureExample.syncToExternalSystem(156 new Set<Id>{[SELECT Id FROM Account LIMIT 1].Id}157 );158 Test.stopTest();159160 // Verify the callout was processed161 System.assert(true, 'Callout completed without errors');162 }163}164165// 3. HTTP Callout Mock166@isTest167public 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}176177// 4. Testing Platform Events178@isTest179private class OrderEventTest {180181 @isTest182 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 );188189 Test.startTest();190 Database.SaveResult sr = EventBus.publish(event);191 Test.stopTest();192193 System.assert(sr.isSuccess(), 'Event should publish successfully');194 }195196 @isTest197 static void testEventSubscriber() {198 // Deliver events in test context199 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 );204205 Test.startTest();206 EventBus.publish(event);207 Test.getEventBus().deliver(); // Force delivery in test208 Test.stopTest();209210 // Verify subscriber created tasks211 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:
- Create a TestDataFactory with methods for Account, Contact, Opportunity, Case, and User
- Write a test class with @TestSetup that creates shared data for 5 test methods
- Write a bulk test that inserts 200 records and verifies trigger behavior
- Implement a mock HTTP callout and test a class that makes external API calls
- Write negative test cases that verify exceptions are thrown for invalid input
- Test a sharing scenario using System.runAs() with Standard User and Admin profiles
- Write a test for a Batch Apex class — verify start(), execute(), and finish()
- Test Platform Event publishing and subscriber behavior using Test.getEventBus().deliver()
- Achieve 95%+ coverage for a complex trigger handler with all 7 trigger events
- 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.