mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
340 lines
13 KiB
OpenEdge ABL
340 lines
13 KiB
OpenEdge ABL
/**
|
|
* 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<SObject> records);
|
|
void doUpdate(SObject record);
|
|
void doUpdate(List<SObject> records);
|
|
void doUpsert(SObject record);
|
|
void doUpsert(List<SObject> records);
|
|
void doDelete(SObject record);
|
|
void doDelete(List<SObject> 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<SObject> records) {
|
|
if (records != null && !records.isEmpty()) {
|
|
insert records;
|
|
}
|
|
}
|
|
|
|
public void doUpdate(SObject record) {
|
|
update record;
|
|
}
|
|
|
|
public void doUpdate(List<SObject> records) {
|
|
if (records != null && !records.isEmpty()) {
|
|
update records;
|
|
}
|
|
}
|
|
|
|
public void doUpsert(SObject record) {
|
|
upsert record;
|
|
}
|
|
|
|
public void doUpsert(List<SObject> records) {
|
|
if (records != null && !records.isEmpty()) {
|
|
upsert records;
|
|
}
|
|
}
|
|
|
|
public void doDelete(SObject record) {
|
|
delete record;
|
|
}
|
|
|
|
public void doDelete(List<SObject> 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<SObject> InsertedRecords = new List<SObject>();
|
|
public static List<SObject> UpdatedRecords = new List<SObject>();
|
|
public static List<SObject> UpsertedRecords = new List<SObject>();
|
|
public static List<SObject> DeletedRecords = new List<SObject>();
|
|
|
|
// Counter for generating unique fake IDs
|
|
private static Integer idCounter = 1;
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
// DML OPERATIONS
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
public void doInsert(SObject record) {
|
|
doInsert(new List<SObject>{ record });
|
|
}
|
|
|
|
public void doInsert(List<SObject> 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<SObject>{ record });
|
|
}
|
|
|
|
public void doUpdate(List<SObject> records) {
|
|
if (records != null && !records.isEmpty()) {
|
|
UpdatedRecords.addAll(records);
|
|
}
|
|
}
|
|
|
|
public void doUpsert(SObject record) {
|
|
doUpsert(new List<SObject>{ record });
|
|
}
|
|
|
|
public void doUpsert(List<SObject> 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<SObject>{ record });
|
|
}
|
|
|
|
public void doDelete(List<SObject> 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<SObject> getInsertedOfType(Schema.SObjectType sObjectType) {
|
|
List<SObject> result = new List<SObject>();
|
|
for (SObject record : InsertedRecords) {
|
|
if (record.getSObjectType() == sObjectType) {
|
|
result.add(record);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get updated records of a specific SObject type
|
|
*/
|
|
public static List<SObject> getUpdatedOfType(Schema.SObjectType sObjectType) {
|
|
List<SObject> result = new List<SObject>();
|
|
for (SObject record : UpdatedRecords) {
|
|
if (record.getSObjectType() == sObjectType) {
|
|
result.add(record);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get deleted records of a specific SObject type
|
|
*/
|
|
public static List<SObject> getDeletedOfType(Schema.SObjectType sObjectType) {
|
|
List<SObject> result = new List<SObject>();
|
|
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<Account> 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');
|
|
}
|
|
}
|