-
Notifications
You must be signed in to change notification settings - Fork 0
Mocking SOQL Queries
Using DatabaseLayer.Soql, you can easily engage mocks to test your code without directly interacting with the Salesforce database.
Read on below, and check out these resources for more information:
Follow this process to mock queries in an @IsTest context:
- Ensure all SOQL statements in the code path to be tested use
DatabaseLayer.Soqlclass. Standard SOQL operations, via inline SOQL queries orDatabase.querymethods cannot be mocked. - Call
DatabaseLayer.useMocks()orDatabaseLayer.useMockSoql()as the first line of your apex test. Now, anySoqloperations will be processed byMockSoqlinstead. - Define logic that tells your mocked queries what to return when they run. See Simulating SOQL Queries below for more details.
- Call the code you want to test. As it runs, the framework will run the
MockSoqllogic you defined, instead of retrieving query results from the Salesforce database.
Example:
DatabaseLayer.useMocks();
// Use the `MockRecord` class to generate a mock account to be returned by the query:
Account account = (Account) new MockRecord(Account.SObjectType)?.withId()?.toSObject();
// All queries globally should return this account:
MockSoql.setGlobalMock().withResults(new List<Account>{ account });
// Run the query - this will use MockSoql:
List<Account> results = (List<Account>) DatabaseLayer.Soql.newQuery(Account.SObjectType)
?.setRowLimit(1)
?.toSoql()
?.query();
Assert.areEqual(1, results?.size(), 'Wrong # of results');
Assert.areEqual(account?.Id, results?.get(0)?.Id, 'Did not return mock account');By default, all MockSoql objects will return an empty list of results, but you can configure your queries to behave in one of the following ways:
- Return a static list of results each time your query runs
- Throw a static
Exceptioneach time your query runs. - Dynamically determine the results of the query, using custom logic in the
MockSoql.Simulatorinterface.
Mock query logic can be defined for all queries encountered during a transaction, via the static MockSoql.setGlobalMock() method. Or, they can be defined for each individual query, via the instance setMock() method.
Both approaches have their own set of benefits and drawbacks:
-
MockSoql.setGlobalMock: (recommended) Allows you to define mocks without exposing queries as top-level class variables, but they are less flexible. If you encounter more than one query, you will likely need to use theMockSoql.Simulatorinterface to handle each query seprately, instead of injecting a static list of results to be returned. -
setMock: Gives the flexibility of defining per-query results to be returned, without using aMockSoql.Simulatorimplementation. In practice, this means allSoqlqueries in your production code must be exposed as top-level class variables, which can be less than ideal for a number of reasons.
We generally recommend the first approach, as it allows Soql queries to be properly encapsulated, while the MockSoql.Simulator interface offers the flexibility needed to handle even the most complex of test scenarios.
DatabaseLayer.useMocks();
Account account = (Account) new MockRecord(Account.SObjectType)?.withId()?.toSObject();
List<Account> accounts = new List<Account>{ account };
Soql query = DatabaseLayer.Soql.newQuery(Account.SObjectType)?.toSoql();
// Inject results for an individual query:
((MockSoql) query)?.setMock()?.withResults(accounts);
// Or, inject results for *all* queries encountered during the transaction:
MockSoql.setGlobalMock().withResults(accounts);The setGlobalMock and setMock methods behave similarly, and support two different modes of mocking, using static or dynamic results.
When 0 arguments are passed to the method, the class returns a MockSoql.StaticResults object. This object has methods to inject the following:
- (withResults): Injects a static list of results to be returned when the query runs.
- ([withError[(./The-MockSoql.StaticResults-Class#withError)): Injects a static Exception to be thrown when the query runs.
Alternatively, you can pass a MockSoql.Simulator object to either of the setMock / setGlobalMock methods. This interface can be used to define dynamic query results to be returned when queries run. Think of this interface as the SOQL equivalent to the System.HttpCalloutMock interface that Apex includes for HTTP Callouts.
DatabaseLayer.useMocks();
Account account = (Account) new MockRecord(Account.SObjectType)?.withId()?.toSObject();
List<Account> accounts = new List<Account>{ account };
Soql query = DatabaseLayer.Soql.newQuery(Account.SObjectType)?.toSoql();
// Inject static results:
MockSoql.setGlobalMock()?.withResults(accounts)
// Inject a System.QueryException:
MockSoql.setGlobalMock()?.withError();
// Inject a custom Exception:
System.Exception customError = new MyCustomError();
MockSoql.setGlobalMock()?.withError(customError);
// Inject dynamic query-mocking logic:
MockSoql.Simulator logic = new MyCustomQueryLogic();
MockSoql.setGlobalMock(logic);Unlike SObjects, Schema.AggregateResults cannot be manually constructed or deserialized; they can only be generated by performing an actual SOQL query that interacts with the database.
For this reason, the Soql class returns its own Soql.AggregateResult objects in aggregate queries. This class wraps the standard Schema.AggregateResult object, and offers access to all its same methods.
In tests, you can construct MockSoql.AggregateResult objects and inject them in to your queries:
DatabaseLayer.useMocks();
MockSoql.AggregateResult agg = new MockSoql.AggregateResult()?.addParameter('numRecords', 100);
List<Soql.AggregateResult> mocks = new List<MockSoql.AggregateResult>{ agg });
MockSoql.setGlobalMock()?.withResults(mocks);
List<Soql.AggregateResult> results = soql?.aggregateQuery();See the reference guide for more information about Soql.AggregateResult and MockSoql.AggregateResult.
The Database.QueryLocator object cannot be mocked in a traditional sense, since it manually constructed, or JSON-deserialized. The only way to create an object of this type is by directly interacting with the Salesforce database, via the Database.getQueryLocator method.
For this reason, Soql's getQueryLocator() method returns a Soql.QueryLocator object, which wraps th standard Database.QueryLocator object and provides access to all its methods. For the most part, developers can interact with this object the same way they would with an ordinary Database.QueryLocator:
Soql soql = DatabaseLayer.Soql.newQuery(Account.SObjectType)?.toSoql();
Soql.QueryLocator locator = query?.getQueryLocator();
String query = locator?.getQuery();
System.Iterator<SObject> iterator = locator?.iterator();There is one limitation to this approach: Certain frameworks (like Database.Batchable) that rely on the underlying Database.QueryLocator object cannot be mocked:
public class MyBatch implements Database.Batchable<SObject> {
public Database.QueryLocator start(Database.BatchableContext ctx) {
Soql.QueryLocator locator = DatabaseLayer.Soql.newQuery(Account.SObjectType)
?.toSoql()
?.getQueryLocator();
// Retrieve the underlying Database.QueryLocator:
return locator?.getLocator();
}
// ...rest of the class omitted for brevity...
}The Soql class's getQueryLocator method returns a Soql.QueryLocator. In a mock context, the underlying Database.QueryLocator will be always be null. This means that the start method will return a null object, causing the batch to fail:
@IsTest
static void cannotMockBatchableQueryLocator() {
DatabaseLayer.useMocks();
MyBatch job = new MyBatch();
Test.startTest();
Database.executeBatch(job);
Test.stopTest();
// ! System.NullPointerException
}You can employ one of the following strategies to work around this:
- Call the batch's
start,execute, andfinishmethods invidually in your unit tests. - Amend the batch's
startmethod to return an iterable object instead; there are some drawbacks to this approach. - Use
System.Queueablejobs paired with aSystem.Finalizerinstead ofDatabase.Batchable.
- Generating Test Records
- Dml
- Soql
- Cmdt
- Plugins
- DatabaseLayer
- Dml
- MockDml
- MockRecord
- Cmdt
- MockCmdt
- MockSoql
-
Soql
- Soql.AggregateResult
- Soql.Aggregation
- Soql.Binder
- Soql.Builder
- Soql.Condition
- Soql.ConditionalLogic
- Soql.Criteria
- Soql.Cursor
- Soql.Function
- Soql.InnerQuery
- Soql.InvalidParameterValueException
- Soql.LogicType
- Soql.NullOrder
- Soql.Operation
- Soql.Operator
- Soql.ParentField
- Soql.PreAndPostProcessor
- Soql.QueryLocator
- Soql.Request
- Soql.Scope
- Soql.Selectable
- Soql.SortDirection
- Soql.SortOrder
- Soql.Subquery
- Soql.TypeOf
- Soql.Usage
- Soql.WhenClause