/** * DML Mocking Framework * * Enables true unit testing by replacing database operations with in-memory tracking. * Tests using this pattern run ~35x faster than tests with actual DML. * * See: references/mocking-patterns.md for usage guidance */ // ═══════════════════════════════════════════════════════════════════════════ // INTERFACE: IDML // Define the contract for DML operations // ═══════════════════════════════════════════════════════════════════════════ /** * Interface for DML operations - enables dependency injection */ public interface IDML { void doInsert(SObject record); void doInsert(List records); void doUpdate(SObject record); void doUpdate(List records); void doUpsert(SObject record); void doUpsert(List records); void doDelete(SObject record); void doDelete(List records); } // ═══════════════════════════════════════════════════════════════════════════ // PRODUCTION: DML // Performs actual database operations // ═══════════════════════════════════════════════════════════════════════════ /** * Production implementation performing real database operations */ public class DML implements IDML { public void doInsert(SObject record) { insert record; } public void doInsert(List records) { if (records != null && !records.isEmpty()) { insert records; } } public void doUpdate(SObject record) { update record; } public void doUpdate(List records) { if (records != null && !records.isEmpty()) { update records; } } public void doUpsert(SObject record) { upsert record; } public void doUpsert(List records) { if (records != null && !records.isEmpty()) { upsert records; } } public void doDelete(SObject record) { delete record; } public void doDelete(List records) { if (records != null && !records.isEmpty()) { delete records; } } } // ═══════════════════════════════════════════════════════════════════════════ // TEST MOCK: DMLMock // Tracks operations without hitting the database // ═══════════════════════════════════════════════════════════════════════════ /** * Mock DML implementation for fast unit tests * * Usage: * DMLMock.reset(); * MyService service = new MyService(new DMLMock()); * service.doSomething(); * Assert.areEqual(1, DMLMock.InsertedRecords.size()); */ @IsTest public class DMLMock implements IDML { // ═══════════════════════════════════════════════════════════════════ // TRACKED OPERATIONS // ═══════════════════════════════════════════════════════════════════ public static List InsertedRecords = new List(); public static List UpdatedRecords = new List(); public static List UpsertedRecords = new List(); public static List DeletedRecords = new List(); // Counter for generating unique fake IDs private static Integer idCounter = 1; // ═══════════════════════════════════════════════════════════════════ // DML OPERATIONS // ═══════════════════════════════════════════════════════════════════ public void doInsert(SObject record) { doInsert(new List{ record }); } public void doInsert(List records) { if (records == null || records.isEmpty()) { return; } for (SObject record : records) { // Generate fake ID to simulate database insert if (record.Id == null) { record.Id = generateFakeId(record.getSObjectType()); } InsertedRecords.add(record); } } public void doUpdate(SObject record) { doUpdate(new List{ record }); } public void doUpdate(List records) { if (records != null && !records.isEmpty()) { UpdatedRecords.addAll(records); } } public void doUpsert(SObject record) { doUpsert(new List{ record }); } public void doUpsert(List records) { if (records != null && !records.isEmpty()) { for (SObject record : records) { if (record.Id == null) { record.Id = generateFakeId(record.getSObjectType()); } } UpsertedRecords.addAll(records); } } public void doDelete(SObject record) { doDelete(new List{ record }); } public void doDelete(List records) { if (records != null && !records.isEmpty()) { DeletedRecords.addAll(records); } } // ═══════════════════════════════════════════════════════════════════ // UTILITY METHODS // ═══════════════════════════════════════════════════════════════════ /** * Reset all tracked operations - call at start of each test */ public static void reset() { InsertedRecords.clear(); UpdatedRecords.clear(); UpsertedRecords.clear(); DeletedRecords.clear(); idCounter = 1; } /** * Generate a valid Salesforce ID for testing * Uses the object's key prefix for realistic IDs */ private static Id generateFakeId(Schema.SObjectType sObjectType) { String keyPrefix = sObjectType.getDescribe().getKeyPrefix(); if (keyPrefix == null) { keyPrefix = '000'; // Fallback for objects without prefix } String idBody = String.valueOf(idCounter++).leftPad(12, '0'); return Id.valueOf(keyPrefix + idBody); } // ═══════════════════════════════════════════════════════════════════ // HELPER METHODS FOR ASSERTIONS // ═══════════════════════════════════════════════════════════════════ /** * Get inserted records of a specific SObject type */ public static List getInsertedOfType(Schema.SObjectType sObjectType) { List result = new List(); for (SObject record : InsertedRecords) { if (record.getSObjectType() == sObjectType) { result.add(record); } } return result; } /** * Get updated records of a specific SObject type */ public static List getUpdatedOfType(Schema.SObjectType sObjectType) { List result = new List(); for (SObject record : UpdatedRecords) { if (record.getSObjectType() == sObjectType) { result.add(record); } } return result; } /** * Get deleted records of a specific SObject type */ public static List getDeletedOfType(Schema.SObjectType sObjectType) { List result = new List(); for (SObject record : DeletedRecords) { if (record.getSObjectType() == sObjectType) { result.add(record); } } return result; } /** * Check if any records of a specific type were inserted */ public static Boolean wasInserted(Schema.SObjectType sObjectType) { return !getInsertedOfType(sObjectType).isEmpty(); } /** * Check if any records of a specific type were updated */ public static Boolean wasUpdated(Schema.SObjectType sObjectType) { return !getUpdatedOfType(sObjectType).isEmpty(); } /** * Check if any records of a specific type were deleted */ public static Boolean wasDeleted(Schema.SObjectType sObjectType) { return !getDeletedOfType(sObjectType).isEmpty(); } } // ═══════════════════════════════════════════════════════════════════════════ // EXAMPLE: Service Using Injected DML // ═══════════════════════════════════════════════════════════════════════════ /** * Example service class demonstrating DML injection */ public class AccountService { private IDML dml; // Production constructor - uses real DML public AccountService() { this(new DML()); } // Test constructor - accepts mock DML @TestVisible private AccountService(IDML dml) { this.dml = dml; } public Id createAccount(Account acc) { if (acc == null) { throw new IllegalArgumentException('Account cannot be null'); } dml.doInsert(acc); return acc.Id; } public void updateAccounts(List accounts) { dml.doUpdate(accounts); } } // ═══════════════════════════════════════════════════════════════════════════ // EXAMPLE: Test Class Using DML Mock // ═══════════════════════════════════════════════════════════════════════════ @IsTest private class AccountServiceTest { @IsTest static void testCreateAccount_Success() { // Arrange DMLMock.reset(); AccountService service = new AccountService(new DMLMock()); Account testAcc = new Account(Name = 'Test Corp', Industry = 'Technology'); // Act Test.startTest(); Id accountId = service.createAccount(testAcc); Test.stopTest(); // Assert Assert.isNotNull(accountId, 'Should have generated ID'); Assert.areEqual(1, DMLMock.InsertedRecords.size(), 'Should have 1 insert'); Assert.isTrue(DMLMock.wasInserted(Account.SObjectType), 'Account was inserted'); Account inserted = (Account) DMLMock.InsertedRecords[0]; Assert.areEqual('Test Corp', inserted.Name, 'Name should match'); Assert.areEqual('Technology', inserted.Industry, 'Industry should match'); } @IsTest static void testCreateAccount_NullInput_ThrowsException() { // Arrange DMLMock.reset(); AccountService service = new AccountService(new DMLMock()); // Act/Assert try { service.createAccount(null); Assert.fail('Expected IllegalArgumentException'); } catch (IllegalArgumentException e) { Assert.isTrue(e.getMessage().contains('null'), 'Error should mention null'); } // Verify no DML occurred Assert.areEqual(0, DMLMock.InsertedRecords.size(), 'No records should be inserted'); } }