Skip to content

Commit 4aee5dd

Browse files
authored
AggregateResult mocking (#179)
* AggregateResult mocking draft Signed-off-by: Piotr PG Gajek <[email protected]> * toAggregatedProxy test Signed-off-by: Piotr PG Gajek <[email protected]> * aggregate result mocking Signed-off-by: Piotr PG Gajek <[email protected]> --------- Signed-off-by: Piotr PG Gajek <[email protected]>
1 parent 8eef47e commit 4aee5dd

File tree

5 files changed

+263
-6
lines changed

5 files changed

+263
-6
lines changed

force-app/main/default/classes/main/standard-soql/SOQL.cls

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ public virtual inherited sharing class SOQL implements Queryable {
152152
SObject toObject();
153153
List<SObject> toList();
154154
List<AggregateResult> toAggregated();
155+
List<SOQL.AggregateResultProxy> toAggregatedProxy();
155156
Map<Id, SObject> toMap();
156157
Map<String, SObject> toMap(SObjectField keyField);
157158
Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField);
@@ -328,6 +329,11 @@ public virtual inherited sharing class SOQL implements Queryable {
328329
Boolean hasValue();
329330
}
330331

332+
public interface AggregateResultProxy {
333+
Object get(String field);
334+
Map<String, Object> getPopulatedFieldsAsMap();
335+
}
336+
331337
public static SubQuery SubQuery {
332338
get { return new SoqlSubQuery(); }
333339
}
@@ -379,6 +385,9 @@ public virtual inherited sharing class SOQL implements Queryable {
379385
// SObject
380386
Mockable thenReturn(SObject record);
381387
Mockable thenReturn(List<SObject> records);
388+
// AggregateResultProxy
389+
Mockable thenReturn(List<Map<String, Object>> aggregatedResults);
390+
Mockable thenReturn(Map<String, Object> aggregatedResult);
382391
// Count
383392
Mockable thenReturn(Integer count);
384393
}
@@ -947,6 +956,10 @@ public virtual inherited sharing class SOQL implements Queryable {
947956
return (List<AggregateResult>) this.toList();
948957
}
949958

959+
public List<AggregateResultProxy> toAggregatedProxy() {
960+
return this.executor.toAggregatedProxy();
961+
}
962+
950963
public Map<Id, SObject> toMap() {
951964
return this.converter.transform(this.executor.toList()).toMap();
952965
}
@@ -2268,9 +2281,67 @@ public virtual inherited sharing class SOQL implements Queryable {
22682281
}
22692282
}
22702283

2284+
private class SoqlAggregateResultProxy implements AggregateResultProxy {
2285+
private Map<String, Object> aggregateResult;
2286+
2287+
public SoqlAggregateResultProxy(AggregateResult aggregateResult) {
2288+
this.aggregateResult = aggregateResult.getPopulatedFieldsAsMap();
2289+
}
2290+
2291+
public SoqlAggregateResultProxy(Map<String, Object> aggregateResult) {
2292+
this.aggregateResult = aggregateResult;
2293+
}
2294+
2295+
public Object get(String field) {
2296+
return this.aggregateResult.get(field);
2297+
}
2298+
2299+
public Map<String, Object> getPopulatedFieldsAsMap() {
2300+
return this.aggregateResult;
2301+
}
2302+
}
2303+
2304+
private class AggregateResultProxys {
2305+
private List<AggregateResultProxy> aggregateResults = new List<AggregateResultProxy>();
2306+
2307+
public AggregateResultProxys add(List<AggregateResult> aggregateResults) {
2308+
for (AggregateResult result : aggregateResults) {
2309+
this.aggregateResults.add(new SoqlAggregateResultProxy(result));
2310+
}
2311+
return this;
2312+
}
2313+
2314+
public AggregateResultProxys add(List<Map<String, Object>> aggregateResults) {
2315+
for (Map<String, Object> result : aggregateResults) {
2316+
this.add(result);
2317+
}
2318+
return this;
2319+
}
2320+
2321+
public AggregateResultProxys add(Map<String, Object> aggregateResult) {
2322+
this.aggregateResults.add(new SoqlAggregateResultProxy(aggregateResult));
2323+
return this;
2324+
}
2325+
2326+
public List<AggregateResultProxy> get() {
2327+
return this.aggregateResults;
2328+
}
2329+
}
2330+
22712331
private class SoqlMock implements Mockable {
22722332
public SObjectMock sObjectMock = new SObjectMock();
22732333
public CountMock countMock = new CountMock();
2334+
public AggregateResultProxys aggregateResultMock = new AggregateResultProxys();
2335+
2336+
public Mockable thenReturn(List<Map<String, Object>> aggregatedResults) {
2337+
this.aggregateResultMock.add(aggregatedResults);
2338+
return this;
2339+
}
2340+
2341+
public Mockable thenReturn(Map<String, Object> aggregatedResult) {
2342+
this.aggregateResultMock.add(aggregatedResult);
2343+
return this;
2344+
}
22742345

22752346
public Mockable thenReturn(SObject record) {
22762347
this.sObjectMock.add(record);
@@ -2311,7 +2382,7 @@ public virtual inherited sharing class SOQL implements Queryable {
23112382
}
23122383

23132384
if (!fields.aggregateFields.isEmpty()) {
2314-
throw new QueryException('Aggregate functions mocking is currently not supported.');
2385+
throw new QueryException('Use toAggregatedProxy() to mock AggregateResult records.');
23152386
}
23162387

23172388
this.addIdToMockedRecords();
@@ -2347,9 +2418,6 @@ public virtual inherited sharing class SOQL implements Queryable {
23472418
for (String field : requestedFields) {
23482419
filteredFields.put(field, originalFields.get(field) ?? null);
23492420
}
2350-
if(originalFields.containsKey('Id')) {
2351-
filteredFields.put('Id', originalFields.get('Id'));
2352-
}
23532421

23542422
// JSON.serialize and JSON.deserialize are used to copy not writtable fields
23552423
cleanedRecords.add((SObject) JSON.deserialize(JSON.serialize(filteredFields), objectTypeName));
@@ -2435,6 +2503,15 @@ public virtual inherited sharing class SOQL implements Queryable {
24352503
).getRecords();
24362504
}
24372505

2506+
public List<AggregateResultProxy> toAggregatedProxy() {
2507+
if (this.mock != null) {
2508+
this.incrementQueryIssued();
2509+
return this.mock.aggregateResultMock.get();
2510+
}
2511+
2512+
return new AggregateResultProxys().add((List<AggregateResult>) this.toList()).get();
2513+
}
2514+
24382515
public Integer toInteger() {
24392516
this.incrementQueryIssued();
24402517

force-app/main/default/classes/main/standard-soql/SOQL_Test.cls

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,21 @@ private class SOQL_Test {
20772077
Assert.areEqual(1, results.size(), 'Only one aggregated result should be returned, as the accounts were created by the same user.');
20782078
}
20792079

2080+
@IsTest
2081+
static void groupByRelatedToAggregatedProxy() {
2082+
// Setup
2083+
insertAccounts();
2084+
2085+
// Test
2086+
List<SOQL.AggregateResultProxy> results = SOQL.of(Account.SObjectType)
2087+
.count(Account.Name, 'names')
2088+
.groupBy('Account.CreatedBy', User.Id)
2089+
.toAggregatedProxy();
2090+
2091+
// Verify
2092+
Assert.areEqual(1, results.size(), 'Only one aggregated result should be returned, as the accounts were created by the same user.');
2093+
}
2094+
20802095
@IsTest
20812096
static void groupByRollup() {
20822097
// Test
@@ -3456,6 +3471,7 @@ private class SOQL_Test {
34563471

34573472
@IsTest
34583473
static void mockingMultipleRecords() {
3474+
// Setup
34593475
List<Account> testAccounts = new List<Account>{
34603476
new Account(Name = 'Test 1'),
34613477
new Account(Name = 'Test 2')
@@ -3614,7 +3630,7 @@ private class SOQL_Test {
36143630

36153631
// Verify
36163632
Assert.isNotNull(soqlException, 'Query mocking exception should be thrown.');
3617-
Assert.areEqual('Aggregate functions mocking is currently not supported.', soqlException.getMessage(), 'Mocking field aliasing is not supproted');
3633+
Assert.areEqual('Use toAggregatedProxy() to mock AggregateResult records.', soqlException.getMessage(), 'Mocking field aliasing is not supproted');
36183634
}
36193635

36203636
@IsTest
@@ -3638,7 +3654,7 @@ private class SOQL_Test {
36383654

36393655
// Verify
36403656
Assert.isNotNull(soqlException, 'Query mocking exception should be thrown.');
3641-
Assert.areEqual('Aggregate functions mocking is currently not supported.', soqlException.getMessage(), 'Mocking field aliasing is not supproted');
3657+
Assert.areEqual('Use toAggregatedProxy() to mock AggregateResult records.', soqlException.getMessage(), 'Mocking field aliasing is not supproted');
36423658
}
36433659

36443660
@IsTest
@@ -3667,6 +3683,60 @@ private class SOQL_Test {
36673683
}
36683684
}
36693685

3686+
@IsTest
3687+
static void mockWithAggregateResultProxy() {
3688+
// Setup
3689+
Map<String, Object> aggregateResult = new Map<String, Object>{ 'LeadSource' => 'Web', 'total' => 10};
3690+
3691+
// Test
3692+
SOQL.mock('mockingQuery').thenReturn(aggregateResult);
3693+
3694+
List<SOQL.AggregateResultProxy> results = SOQL.of(Lead.SObjectType)
3695+
.with(Lead.LeadSource)
3696+
.COUNT(Lead.Id, 'total')
3697+
.groupBy(Lead.LeadSource)
3698+
.mockId('mockingQuery')
3699+
.toAggregatedProxy();
3700+
3701+
// Verify
3702+
Assert.areEqual(1, results.size(), 'The size of the aggregate results should match the mocked size.');
3703+
3704+
SOQL.AggregateResultProxy result = results[0];
3705+
3706+
Assert.isNotNull(result.getPopulatedFieldsAsMap(), 'AggregateResult should contain populated fields.');
3707+
Assert.areEqual(10, result.get('total'), 'AggregateResult should contain the total field.');
3708+
Assert.areEqual('Web', result.get('LeadSource'), 'AggregateResult should contain the LeadSource field.');
3709+
}
3710+
3711+
@IsTest
3712+
static void mockWithAggregateResultsProxy() {
3713+
// Setup
3714+
List<Map<String, Object>> aggregateResults = new List<Map<String, Object>>{
3715+
new Map<String, Object>{ 'LeadSource' => 'Web', 'total' => 10},
3716+
new Map<String, Object>{ 'LeadSource' => 'Phone', 'total' => 5},
3717+
new Map<String, Object>{ 'LeadSource' => 'Email', 'total' => 3}
3718+
};
3719+
3720+
// Test
3721+
SOQL.mock('mockingQuery').thenReturn(aggregateResults);
3722+
3723+
List<SOQL.AggregateResultProxy> result = SOQL.of(Lead.SObjectType)
3724+
.with(Lead.LeadSource)
3725+
.COUNT(Lead.Id, 'total')
3726+
.groupBy(Lead.LeadSource)
3727+
.mockId('mockingQuery')
3728+
.toAggregatedProxy();
3729+
3730+
// Verify
3731+
Assert.areEqual(3, result.size(), 'The size of the aggregate results should match the mocked size.');
3732+
3733+
for (SOQL.AggregateResultProxy aggregateResult : result) {
3734+
Assert.isNotNull(aggregateResult.getPopulatedFieldsAsMap(), 'AggregateResult should contain populated fields.');
3735+
Assert.isNotNull(aggregateResult.get('total'), 'AggregateResult should contain the total field.');
3736+
Assert.isNotNull(aggregateResult.get('LeadSource'), 'AggregateResult should contain the LeadSource field.');
3737+
}
3738+
}
3739+
36703740
@IsTest
36713741
static void toId() {
36723742
// Setup

website/docs/advanced-usage/mocking.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,47 @@ private class ExampleControllerTest {
275275
}
276276
```
277277

278+
## AggregateResult
279+
280+
There is no way to create a `new AggregateResult()` instance in Apex. You can find more details here: [AggregateResult mocking consideration](https://github.com/beyond-the-cloud-dev/soql-lib/discussions/171).
281+
282+
To mock `AggregateResult`, we introduced `SOQL.AggregateResultProxy`, which provides the same methods as the standard `AggregateResult` class.
283+
284+
```apex
285+
public with sharing class ExampleController {
286+
public void getLeadAggregateResults() {
287+
List<SOQL.AggregateResultProxy> result = SOQL.of(Lead.SObjectType)
288+
.with(Lead.LeadSource)
289+
.COUNT(Lead.Id, 'total')
290+
.groupBy(Lead.LeadSource)
291+
.mockId('ExampleController.getLeadAggregateResults')
292+
.toAggregatedProxy(); // <== use toAggregatedProxy()
293+
}
294+
}
295+
296+
297+
@IsTest
298+
public class ExampleControllerTest {
299+
@IsTest
300+
static void getLeadAggregateResults() {
301+
List<Map<String, Object>> aggregateResults = new List<Map<String, Object>>{
302+
new Map<String, Object>{ 'LeadSource' => 'Web', 'total' => 10},
303+
new Map<String, Object>{ 'LeadSource' => 'Phone', 'total' => 5},
304+
new Map<String, Object>{ 'LeadSource' => 'Email', 'total' => 3}
305+
};
306+
307+
SOQL.mock('ExampleController.getLeadAggregateResults').thenReturn(aggregateResults);
308+
309+
Test.startTest();
310+
List<SOQL.AggregateResultProxy> result = ExampleController.getLeadAggregateResults();
311+
Test.stopTest();
312+
313+
// Assert
314+
Assert.areEqual(3, result.size(), 'The size of the aggregate results should match the mocked size.');
315+
}
316+
}
317+
```
318+
278319
## No Results
279320

280321
Pass an empty list: `.thenReturn(new List<Type>())`;

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,6 +2111,34 @@ SOQL.of(Account.sObjectType)
21112111
SOQL.mock('MyQuery).thenReturn(5);
21122112
```
21132113

2114+
### aggregateResult mock
2115+
2116+
**Signature**
2117+
2118+
```apex
2119+
SOQL.Mockable mock(String mockId).thenReturn(List<Map<String, Object>> mock);
2120+
SOQL.Mockable mock(String mockId).thenReturn(Map<String, Object> mock);
2121+
```
2122+
2123+
**Example**
2124+
2125+
```apex
2126+
List<Map<String, Object>> aggregateResults = new List<Map<String, Object>>{
2127+
new Map<String, Object>{ 'LeadSource' => 'Web', 'total' => 10},
2128+
new Map<String, Object>{ 'LeadSource' => 'Phone', 'total' => 5},
2129+
new Map<String, Object>{ 'LeadSource' => 'Email', 'total' => 3}
2130+
};
2131+
2132+
SOQL.mock('mockingQuery').thenReturn(aggregateResults);
2133+
2134+
List<SOQL.AggregateResultProxy> result = SOQL.of(Lead.SObjectType)
2135+
.with(Lead.LeadSource)
2136+
.COUNT(Lead.Id, 'total')
2137+
.groupBy(Lead.LeadSource)
2138+
.mockId('mockingQuery')
2139+
.toAggregatedProxy();
2140+
```
2141+
21142142
## DEBUGGING
21152143
### preview
21162144

website/docs/examples/standard-soql/mocking.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,47 @@ public class ExampleControllerTest {
5353

5454
During execution Selector will return record that was set by `.thenReturn` method.
5555

56+
57+
## Mock AggregateResult
58+
59+
To mock `AggregateResult` - use `toAggregatedProxy()`.
60+
61+
```apex
62+
public with sharing class ExampleController {
63+
public void getLeadAggregateResults() {
64+
List<SOQL.AggregateResultProxy> result = SOQL.of(Lead.SObjectType)
65+
.with(Lead.LeadSource)
66+
.COUNT(Lead.Id, 'total')
67+
.groupBy(Lead.LeadSource)
68+
.mockId('ExampleController.getLeadAggregateResults')
69+
.toAggregatedProxy(); // <== use toAggregatedProxy()
70+
}
71+
}
72+
```
73+
74+
```apex
75+
@IsTest
76+
public class ExampleControllerTest {
77+
@IsTest
78+
static void getLeadAggregateResults() {
79+
List<Map<String, Object>> aggregateResults = new List<Map<String, Object>>{
80+
new Map<String, Object>{ 'LeadSource' => 'Web', 'total' => 10},
81+
new Map<String, Object>{ 'LeadSource' => 'Phone', 'total' => 5},
82+
new Map<String, Object>{ 'LeadSource' => 'Email', 'total' => 3}
83+
};
84+
85+
SOQL.mock('ExampleController.getLeadAggregateResults').thenReturn(aggregateResults);
86+
87+
Test.startTest();
88+
List<SOQL.AggregateResultProxy> result = ExampleController.getLeadAggregateResults();
89+
Test.stopTest();
90+
91+
// Assert
92+
Assert.areEqual(3, result.size(), 'The size of the aggregate results should match the mocked size.');
93+
}
94+
}
95+
```
96+
5697
## Mock No Results
5798

5899
```apex

0 commit comments

Comments
 (0)