Skip to content

Commit c6babe0

Browse files
committed
documentation update
1 parent 1e2554a commit c6babe0

File tree

4 files changed

+247
-29
lines changed

4 files changed

+247
-29
lines changed

force-app/main/default/classes/main/cached-soql/SOQLCache_Test.cls

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,23 @@ private class SOQLCache_Test {
959959
Assert.isNull(profile, 'The profile should be null.');
960960
}
961961

962+
@IsTest
963+
static void mockEmptyRecord() {
964+
// Setup
965+
SOQLCache.mock('ProfileQuery').thenReturn(null);
966+
967+
// Test
968+
Profile profile = (Profile) SOQLCache.of(Profile.SObjectType)
969+
.with(Profile.Id, Profile.Name)
970+
.mockId('ProfileQuery')
971+
.whereEqual(Profile.Name, 'Profile That Not Exist')
972+
.toObject();
973+
974+
// Verify
975+
Assert.isNull(profile, 'The profile should be null.');
976+
Assert.areEqual(0, Limits.getQueries(), 'No query should be issued.');
977+
}
978+
962979
@IsTest
963980
static void cachedRecordDoesNotHaveNecessaryFields() {
964981
// Test
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
---
2+
sidebar_position: 30
3+
---
4+
5+
# Mocking
6+
7+
Mocking provides a way to substitute records from a database with some prepared data. Data can be prepared in the form of SObject records and lists in Apex code or Static Resource `.csv` file.
8+
Mocked queries won't make any SOQLs and simply return data set in method definition. Mock __will ignore all filters and relations__; what is returned depends __solely on data provided to the method__. Mocking works __only during test execution__. To mock a cached query, use the `.mockId(id)` method to make it identifiable. If you mark more than one query with the same ID, all marked queries will return the same data.
9+
10+
```apex title="ExampleController.cls"
11+
public with sharing class ExampleController {
12+
public static Account getPartnerAccount(String accountName) {
13+
return (Account) SOQLCache.of(Account.SObjectType)
14+
.with(Account.Id, Account.Name, Account.BillingCity)
15+
.whereEqual(Account.Name, accountName)
16+
.mockId('ExampleController.getPartnerAccount')
17+
.toObject();
18+
}
19+
20+
public static Account getAccountWithParent(String accountName) {
21+
return (Account) SOQLCache.of(Account.SObjectType)
22+
.with(Account.Id, Account.Name)
23+
.with('Parent', Account.Name)
24+
.whereEqual(Account.Name, accountName)
25+
.mockId('ExampleController.getAccountWithParent')
26+
.toObject();
27+
}
28+
29+
public static Account getAccountByName(String accountName) {
30+
return (Account) SOQLCache.of(Account.SObjectType)
31+
.with(Account.Id, Account.Name)
32+
.whereEqual(Account.Name, accountName)
33+
.mockId('ExampleController.getAccountByName')
34+
.toObject();
35+
}
36+
}
37+
```
38+
39+
Then in tests simply pass data you want to get from the cached query to the `SOQLCache.mock(id).thenReturn(data)` method. Acceptable formats are: `SObject`. During execution, the cached query will return the desired data.
40+
41+
## Insights
42+
43+
### Mocking Stack Functionality
44+
45+
SOQL Cache implements a sophisticated mocking system that supports multiple sequential mocks for the same query identifier. This enables complex testing scenarios where the same cached query needs to return different results across multiple executions.
46+
47+
**How the Stack Works:**
48+
49+
Each call to `SOQLCache.mock(mockId)` creates a new mock entry and adds it to a list (stack) associated with that mock ID. When cached queries are executed:
50+
51+
- **Single Mock**: If only one mock exists for the ID, it's reused for all executions
52+
- **Multiple Mocks**: Mocks are consumed in FIFO (First In, First Out) order - each execution removes and returns the first mock from the stack
53+
54+
```apex title="Mock Stack Example"
55+
// Setup multiple sequential mocks
56+
SOQLCache.mock('testQuery').thenReturn(new Account(Name = 'First Call'));
57+
SOQLCache.mock('testQuery').thenReturn(new Account(Name = 'Second Call'));
58+
SOQLCache.mock('testQuery').thenReturn(new Account(Name = 'Third Call'));
59+
60+
// First execution returns "First Call", then removes that mock
61+
Account result1 = (Account) SOQLCache.of(Account.SObjectType)
62+
.with(Account.Name)
63+
.whereEqual(Account.Name, 'Test')
64+
.mockId('testQuery')
65+
.toObject();
66+
67+
// Second execution returns "Second Call", then removes that mock
68+
Account result2 = (Account) SOQLCache.of(Account.SObjectType)
69+
.with(Account.Name)
70+
.whereEqual(Account.Name, 'Test')
71+
.mockId('testQuery')
72+
.toObject();
73+
74+
// Third execution returns "Third Call", but does not remove that mock - it's the last mock on the stack
75+
Account result3 = (Account) SOQLCache.of(Account.SObjectType)
76+
.with(Account.Name)
77+
.whereEqual(Account.Name, 'Test')
78+
.mockId('testQuery')
79+
.toObject();
80+
81+
// Fourth execution returns "Third Call" - it's the last mock on the stack
82+
Account result4 = (Account) SOQLCache.of(Account.SObjectType)
83+
.with(Account.Name)
84+
.whereEqual(Account.Name, 'Test')
85+
.mockId('testQuery')
86+
.toObject();
87+
```
88+
89+
### Id Field Behavior
90+
91+
The `Id` field is always included in mocked cached results, even if it wasn't explicitly specified. This is designed to mirror standard SOQL behavior — Salesforce automatically includes the `Id` field in every query, even when it's not listed in the `SELECT` clause.
92+
93+
```apex title="Standard SOQL Behavior"
94+
List<Account> accounts = [SELECT Name FROM Account LIMIT 3];
95+
System.debug(accounts);
96+
/* Output:
97+
(
98+
Account:{Name=Test 1, Id=001J5000008AvzkIAC},
99+
Account:{Name=Test 2, Id=001J5000008AvzlIAC},
100+
Account:{Name=Test 3, Id=001J5000008AvzmIAC}
101+
)
102+
*/
103+
```
104+
105+
Similarly, when you mock records using SOQLCache:
106+
107+
```apex title="SOQLCache Mock Example"
108+
SOQLCache.mock('mockingQuery').thenReturn(new List<Account>{
109+
new Account(Name = 'Test 1'),
110+
new Account(Name = 'Test 2')
111+
});
112+
113+
List<Account> accounts = (List<Account>) SOQLCache.of(Account.SObjectType)
114+
.with(Account.Name)
115+
.allowQueryWithoutConditions()
116+
.mockId('mockingQuery')
117+
.toObject(); // Note: SOQLCache typically returns single objects
118+
119+
/* Output includes Id even though not specified:
120+
(
121+
Account:{Name=Test 1, Id=001J5000008AvzkIAC},
122+
Account:{Name=Test 2, Id=001J5000008AvzlIAC}
123+
)
124+
*/
125+
```
126+
127+
Even though `Id` wasn't specified in `.with()`, it's automatically added.
128+
129+
## Single record
130+
131+
```apex title="ExampleControllerTest.cls"
132+
@IsTest
133+
private class ExampleControllerTest {
134+
135+
@IsTest
136+
static void getPartnerAccount() {
137+
SOQLCache.mock('ExampleController.getPartnerAccount').thenReturn(new Account(Name = 'MyAccount 1'));
138+
139+
// Test
140+
Account result = ExampleController.getPartnerAccount('MyAccount');
141+
142+
Assert.areEqual('MyAccount 1', result.Name);
143+
}
144+
}
145+
```
146+
147+
## Parent relationship
148+
149+
```apex title="ExampleControllerTest.cls"
150+
@IsTest
151+
private class ExampleControllerTest {
152+
@IsTest
153+
static void getAccountWithParent() {
154+
SOQLCache.mock('ExampleController.getAccountWithParent').thenReturn(
155+
new Account(
156+
Name = 'Test Account',
157+
Parent = new Account(Name = 'Parent Account')
158+
)
159+
);
160+
161+
// Test
162+
Account result = ExampleController.getAccountWithParent('Test Account');
163+
164+
Assert.areEqual('Test Account', result.Name);
165+
Assert.areEqual('Parent Account', result.Parent.Name);
166+
}
167+
}
168+
```
169+
170+
## No Results
171+
172+
Pass an empty list or `null`: `.thenReturn(null)`;
173+
- When `.toObject()` is invoked, it will return `null`.
174+
175+
This behavior will be the same as it is during runtime.
176+
177+
```apex title="ExampleControllerTest.cls"
178+
@IsTest
179+
public class ExampleControllerTest {
180+
private static final String TEST_ACCOUNT_NAME = 'MyAccount 1';
181+
182+
@IsTest
183+
static void getAccountByName() {
184+
SOQLCache.mock('ExampleController.getAccountByName')
185+
.thenReturn(null);
186+
187+
Test.startTest();
188+
Account result = ExampleController.getAccountByName(TEST_ACCOUNT_NAME);
189+
Test.stopTest();
190+
191+
Assert.isNull(result);
192+
}
193+
}
194+
```

website/docs/cache/api/soql-cache.md

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ The following are methods for using `SOQLCache`:
9292

9393
### cacheInApexTransaction
9494

95+
Queried records are stored in Apex cache (static variable) only for one Apex Transaction.
96+
Very useful when data doesn't change often during one transaction like `User`.
97+
98+
:::info[Default]
99+
100+
Apex Transaction is the default cache storage when none is specified.
101+
102+
:::
103+
95104
**Signature**
96105

97106
```apex title="Method Signature"
@@ -257,7 +266,7 @@ public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCac
257266
super(Profile.SObjectType);
258267
cacheInOrgCache();
259268
allowQueryWithoutConditions();
260-
with(Profile.Id, Profile.Name, Profile.UserType)
269+
with(Profile.Id, Profile.Name, Profile.UserType);
261270
}
262271
263272
public override SOQL.Queryable initialQuery() {
@@ -274,7 +283,7 @@ public with sharing class SOQL_ProfileCache extends SOQLCache implements SOQLCac
274283

275284
## INITIAL QUERY
276285

277-
The initial query allows for the bulk population of records in the cache (if it is empty), ensuring that every subsequent query in the cached selector will use the cached records.
286+
The initial query enables bulk population of records in the cache (if it is empty), ensuring that every subsequent query in the cached selector will use the cached records.
278287

279288
For instance:
280289

@@ -467,18 +476,19 @@ SOQLCache.of(Account.SObjectType)
467476

468477
## WHERE
469478

470-
A cached query must include one condition. The filter must use a cached field (defined in `cachedFields()`) and should be based on `Id`, `Name`, `DeveloperName`, or another unique field.
479+
A cached query must include at least one condition. The filter must use a cached field (defined in `cachedFields()`) and should be based on `Id`, `Name`, `DeveloperName`, or another unique field.
471480

472-
A query requires a single condition, and that condition must filter by a unique field.
481+
The query requires a single condition, and that condition must filter by a unique field.
473482

474483
To ensure that cached records are aligned with the database, a single condition is required.
475484
A query without a condition cannot guarantee that the number of records in the cache matches the database.
476485

477486
For example, let’s assume a developer makes the query: `SELECT Id, Name FROM Profile`. Cached records will be returned, but they may differ from the records in the database.
478487

479-
The filter field should be unique. Consistency issues can arise when the field is not unique. For instance, the query:
488+
The filter field should be unique. Consistency issues can arise when the field is not unique. For instance, consider this query:
480489
`SELECT Id, Name FROM Profile WHERE UserType = 'Standard'`
481-
may return some records, but the number of records in the cache may differ from those in the database.
490+
491+
This query may return some records, but the number of records in the cache may differ from those in the database.
482492

483493
Using a unique field ensures that if a record is not found in the cache, the SOQL library can look it up in the database.
484494

@@ -502,13 +512,13 @@ Using a unique field ensures that if a record is not found in the cache, the SOQ
502512
| 00e3V000000NmeYQAS | Solution Manager | Standard |
503513
| 00e3V000000NmeHQAS | Customer Community Plus User | PowerCustomerSuccess |
504514

505-
Lets assume a developer executes the query:
515+
Let's assume a developer executes this query:
506516
`SELECT Id, Name, UserType FROM Profile WHERE UserType = 'Standard'`.
507517

508518
Since records exist in the cache, 2 records will be returned, which is incorrect. The database contains 4 records with `UserType = 'Standard'`.
509519
To avoid such scenarios, filtering by a unique field is required.
510520

511-
Sometimes, certain limitations can ensure that code functions in a deterministic and expected way. From our perspective, it is better to have limitations that make the code free from bugs and prevent unintended misuse.
521+
Sometimes, certain limitations ensure that code functions in a deterministic and expected way. We believe it's better to have limitations that keep the code bug-free and prevent unintended misuse.
512522

513523
### whereEqual
514524

@@ -562,10 +572,10 @@ SOQLCache.of(Profile.SObjectType)
562572
### mockId
563573

564574
Developers can mock either the query or the cached result:
565-
- `SOQLCache.mock('queryId').thenReturn(record);` mocks cached results.
566-
- `SOQL.mock('queryId').thenReturn(record);` mocks the query when cached records are not found.
575+
- `SOQLCache.mock('queryId').thenReturn(record);` mocks cached results
576+
- `SOQL.mock('queryId').thenReturn(record);` mocks the query when cached records are not found
567577

568-
We generally recommend using `SOQLCache.mock('queryId').thenReturn(record);` to ensure that records from the cache are not returned, which could otherwise lead to test instability.
578+
We generally recommend using `SOQLCache.mock('queryId').thenReturn(record);` to ensure that records from the cache are not returned. This prevents test instability that could otherwise occur.
569579

570580
**Signature**
571581

@@ -583,9 +593,9 @@ SOQLCache.of(Profile.SObjectType)
583593
.toObject();
584594
585595
// In Unit Test
586-
SOQLCache.mock('MyQuery').thenReturn(new Profile(Name = 'Mocked System Adminstrator'));
596+
SOQLCache.mock('MyQuery').thenReturn(new Profile(Name = 'Mocked System Administrator'));
587597
// or
588-
SOQL.mock('MyQuery').thenReturn(new Profile(Name = 'Mocked System Adminstrator'));
598+
SOQL.mock('MyQuery').thenReturn(new Profile(Name = 'Mocked System Administrator'));
589599
```
590600

591601
### record mock
@@ -606,7 +616,7 @@ SOQLCache.of(Profile.SObjectType)
606616
.toObject();
607617
608618
// In Unit Test
609-
SOQLCache.mock('MyQuery').thenReturn(new Profile(Name = 'Mocked System Adminstrator'));
619+
SOQLCache.mock('MyQuery').thenReturn(new Profile(Name = 'Mocked System Administrator'));
610620
```
611621

612622
## DEBUGGING

0 commit comments

Comments
 (0)