Welcome to the apex-database-layer
, a comprehensive toolkit designed to simplify database operations and enhance testing capabilities within Salesforce. This package abstracts standard DML and SOQL operations, offering a more flexible, testable, and mockable approach to handling database interactions in your Apex code.
Whether you're building complex applications or writing robust tests, the apex-database-layer
equips you with tools to streamline your development process. By leveraging this package, you can ensure that your database logic is not only efficient but also easily adaptable to various testing scenarios.
apex-database-layer
is available as an unlocked package. You can find the latest or past versions in the Releases tab.
Use the following command to install the package in your environment:
sf package install -p {{package_version_id}}
This package can be thought of in four categories, each with its own distinct set of responsibilities:
Dml
&MockDml
: Performing DML operationsSoql
&MockSoql
: Performing SOQL operationsDatabaseLayer
: ConstructingDml
andSoql
objectsMockRecord
: Mocking SObject records for test purposes
The Dml
class is responsible for inserting, modifying, and deleting records in the salesforce database. It wraps the relevant methods in the standard Database class, like Database.insert
. Use the Dml
class and its methods in place of these system methods, as demonstrated below:
// Don't use these built-in platform methods
insert records;
Database.insert(records);
// Instead, use the Dml class's methods
DatabaseLayer.Dml.doInsert(records);
In apex tests, you can mock all of your DML operations by calling DatabaseLayer.useMocks()
. This will automatically substitute real Dml
objects with a MockDml
object.
By default this class will simulate successful DML operations:
DatabaseLayer.useMocks();
Account account = new Account(Name = 'Test Account');
DatabaseLayer.Dml.doInsert(account);
Assert.isNotNull(account?.Id, 'Was not inserted');
To simulate DML failures, use the fail()
method:
DatabaseLayer.useMocks();
Account account = new Account(Name = 'Test Account');
MockDml dml = (MockDml) DatabaseLayer.Dml;
dml?.fail();
// All subsuquent dml operations should fail
dml?.doInsert(account);
If necessary, you can inject "smarter" failure logic via the MockDml.ConditionalFailure
interface and the failIf()
method:
public class ExampleFailure implements MockDml.ConditionalFailure {
public Exception checkFailure(MockDml.Operation operation, SObject record) {
// Return an Exception if the record/operation should fail
// In this case, any updated Accounts will fail
if (
operation == MockDml.Operation.DO_UPDATE &&
record?.getSObjectType() == Account.SObjectType
) {
return new System.DmlException();
} else {
return null;
}
}
}
// Inject the conditional logic via the failIf() method
DatabaseLayer.useMocks();
MockDml dml = (MockDml) DatabaseLayer.Dml;
MockDml.ConditionalFailure logic = new ExampleFailure();
dml?.failIf(logic);
// This won't fail, because it's not an update!
dml?.doInsert();
MockDml
does not actually modify records in the database, so you cannot use SOQL to retrieve changes and perform assertions against them. Instead, use history objects, like MockDml.INSERTED
to retrieve modified SObject records in memory:
@IsTest
static void someTest() {
DatabaseLayer.useMocks();
Account acc = new Account(Name = 'John Doe');
Test.startTest();
DatabaseLayer.Dml.doInsert(acc);
Test.stopTest();
List<Account> insertedAccs = MockDml.INSERTED.getRecords(Account.SObjectType);
Assert.areEqual(1, insertedAccs?.size(), 'Account was not inserted');
}
View the docs to learn more about the Dml
and MockDml
classes.
The Soql
class is responsible for querying records from the database. It wraps the standard Database.query
and related methods. You can use its flexible builder pattern to compose a wide range of queries.
Soql soql = (Soql) DatabaseLayer.Soql.newQuery(User.SObjectType)
?.addSelect(User.FirstName)
?.addSelect(User.LastName)
?.addSelect(User.Email)
?.addWhere(User.IsActive, Soql.EQUALS, true)
?.addWhere('Profile.Name', Soql.EQUALS, 'System Administrator')
?.orderBy(User.CreatedDate, Soql.SortDirection.ASCENDING)
?.setRowLimit(1);
List<User> users = soql?.query();
In apex tests, you can mock all of your query operations by calling DatabaseLayer.useMocks()
. This will automatically substitute real Soql
objects with a MockSoql
object.
By default, MockSoql
objects will return an empty list of results. You can inject mock results for each query set using the setMock()
method:
DatabaseLayer.useMocks();
Account account = new Account(Name = 'Test Account');
MockSoql soql = DatabaseLayer.newQuery(Account.SObjectType);
soql?.setMock(new List<Account>{ account });
List<Account> results = (List<Account>) soql?.query();
Assert.areEqual(1, results?.size(), 'Wrong # of results');
Mocking queries by passing the records to be returned (as shown above) should work for most use cases. If needed, you can implement your own custom logic by creating a class that implements the MockSoql.Simulator
interface:
public class MySimulator implements MockSoql.Simulator {
// This implementation generates a List<Opportunity> with random values
public Object simulateQuery() {
Integer numOpps = Integer.valueOf(Math.random() * 200);
List<Opportunity> opps = new List<Opportunity>();
for (Integer i = 0; i < numOpps; i++) {
Opportunity opp = new Opportunity();
opp.Amount = Decimal.valueOf(Math.random() * 10000);
opps?.add(opp);
}
return opps;
}
}
You can pass that object to the setMock()
method, as shown below:
DatabaseLayer.useMocks();
MockSoql.Simulator simulator = new MySimulator();
MockSoql soql = (MockSoql) DatabaseLayer.Soql.newQuery(Opportunity.SObjectType);
soql?.setMock(simulator);
List<Opportunity> opps = soql?.query();
You can also simulate query errors via the setError()
method:
DatabaseLayer.useMocks();
MockSoql soql = DatabaseLayer.newQuery(Account.SObjectType);
soql?.setError();
try {
soql?.query();
Assert.fail('Did not throw an exception');
} catch (Exception error) {
// As expected
}
View the docs to learn more about the Soql
and MockSoql
classes.
The DatabaseLayer
class is responsible for constructing new Dml
and Soql
objects:
Dml myDml = DatabaseLayer.Dml;
Soql mySoql = (Soql) DatabaseLayer.Soql.newQuery(Account.SObjectType);
This approach allows for mocks to be automatically substituted at runtime during tests, if desired. By default, each of these methods will return base implementations of the Dml
and Soql
classes, which directly interact with the Salesforce database. In @IsTest
context, you can use the DatabaseLayer.useMocks()
method. Once this is done, the Dml
and Soql
static properties will reflect mock instances of their respective objects:
@IsTest
static void shouldUseMockDml() {
// Assuming ExampleClass has a Dml property called "dml"
DatabaseLayer.useMocks();
ExampleClass example = new ExampleClass();
Assert.isInstanceOfType(example.dml, MockDml.class, 'Not using mocks');
}
You can also revert the DatabaseLayer
class to use real database operations by calling DatabaseLayer.useRealData()
. This should only be used in cases where some (but not all) database operations should be mocked:
@IsTest
static void shouldUseMixedOfMocksAndRealDml() {
DatabaseLayer.useMocks();
ExampleClass mockExample = new ExampleClass();
Assert.isInstanceOfType(mockExample.dml, MockDml.class, 'Not using mocks');
// Now switch to using real data,
// will apply to any new Dml classes going forward
DatabaseLayer.useRealData();
ExampleClass databaseExample = new ExampleClass();
Assert.isNotInstanceOfType(databaseExample.dml, MockDml.class, 'Using mocks?');
}
While mocking database operations can provide many benefits, mocking SObject records in the absence of real DML or SOQL can be tedious.
The MockRecord
class addresses many of the pains associated with this process, including:
- Set read-only fields (including system-level fields)
- Simulate record inserts
- Simulate parent and child relationship retrievals through SOQL
Use the class's fluent builder pattern to generate a record to your specifications, and then cast it back to a concrete SObject. Example:
Account realAccount = [
SELECT
Id, CreatedDate, Owner.Name,
(SELECT Id FROM Contacts)
FROM Account
LIMIT 1
];
// Let's make a test record that can be used to mock the above query!
User mockUser = (User) new MockRecord(User.SObjectType)
?.setField(User.Name, 'John Doe')
?.withId()
?.toSObject();
Contact mockContact = (Contact) new MockRecord(Contact.SObjectType)
?.withId()
?.toSObject();
Account mockAccount = (Account) new MockRecord(Account.SObjectType)
?.setField(Account.Name, 'John Doe Enterprises')
?.setField(Account.CreatedDate, DateTime.now()?.addDays(-100))
?.setLookup(Account.OwnerId, mockUser)
?.setRelatedList(Contact.AccountId, new List<Contact>{ mockContact })
?.withId()
?.toSObject();