Skip to content

A slim, extensible DML & SOQL Mocking Library.

License

Notifications You must be signed in to change notification settings

jasonsiders/apex-database-layer

Repository files navigation

Apex Database Layer

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.

Getting Started

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}}

Usage

This package can be thought of in four categories, each with its own distinct set of responsibilities:

  • Dml & MockDml: Performing DML operations
  • Soql & MockSoql: Performing SOQL operations
  • DatabaseLayer: Constructing Dml and Soql objects
  • MockRecord: Mocking SObject records for test purposes

Performing DML Operations

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.

Performing SOQL Operations

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.

Constructing Database Objects

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?');
}

Building Test Records

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();

About

A slim, extensible DML & SOQL Mocking Library.

Resources

License

Stars

Watchers

Forks

Languages