The only workable approach that I can come up with here is to introduce an additional layer of abstraction. I got round-trip de/serialization to not throw any errors, but it also didn't contain any of the data I was trying to inject.
So instead of that:
- If you have your query being handled in a Selector class, or even just in its own class instead of defining/executing the query directly in the code that uses the result
- You can create an additional "wrapper" class that you would use instead of using the
AggregateResult
directly
It's the abstraction/indirection introduced by that wrapper class which allows you to work around our inability to do much with AggregateResult
.
Separating out the query/queries into a Selector class is probably the gold standard at the moment, but is not strictly required. The wrapper class though, is required. At its most simple, you could achieve this by having a list of the wrapper class as a static or instance variable, then wrapping the query in question inside of an if
so it only executes when the static/instance variable isn't populated.
public class AggregateRateWrapper {
public Integer rate;
public Id rateId1;
public Id rateId2;
}
public class SystemUnderTest {
// private + testvisible leaves a bit of a sour taste in my mouth, but
// the option is there, and this is simple and prevents abuse.
@TestVisible
private static List<AggregateRateWrapper> wrappedRates;
public void run() {
...
// New-ish Null coalescing operator allows us to handle potential
// nulls more compactly compared to if/else or ternaries
List<AggregateRateWrapper> workingWrappedRates = SystemUnderTest.wrappedRates ?? new List<AggregateRateWrapper>();
// I'm also not super stoked about hiding normal logic behind test-specific
// details.
// At the very least, testing the static var for null here (as
// opposed to testing against workingWrappedRates) allows you to
// get into this block
// for some tests (probably closer to integration/end-to-end tests)
if(SystemUnderTest.wrappedRates == null) {
// run the query as normal, but you also need to wrap the results so later
// logic can work on the same thing whether you're in a test or not
for(AggregateResult ar : [SELECT MAX(Rate__c), Lookup1__c AS rateId, Lookup2__c AS rateId2 FROM MyObject__c GROUP BY Lookup1__c, Lookup2__c]) {
AggregateRateWrapper wrapped = new AggregateRateWrapper();
// More null coalescing
//wrapped.rate = (Double)ar.get('expr0') == null ? 0 : (Double)ar.get('expr0');
wrapped.rate = (Double)ar.get('expr0') ?? 0;
wrapped.rateId = (Id)ar.get('rateId');
wrapped.rateId2 = (Id)ar.get('rateId2');
workingWrappedRates.add(wrapped);
}
}
...
// Instead of using the AggregateResult directly, use the wrapper
//for(AggregateResult ar : [SELECT MAX(Rate__c), Lookup1__c AS rateId, Lookup2__c AS rateId2 FROM MyObject__c GROUP BY Lookup1__c, Lookup2__c]) {
for(AggregateRateWrapper wrappedRate : workingWrappedRates) {
// Logic directly using ar.get() needs to be updated
//if(ar.get('rateId') == null) {
if(wrappedRate.rateId == null) {
throw new SomeException();
}
}
}
}
@isTest
private without sharing class MyTest {
@isTest
static void callRun_withMockAggregateResultData_doesAThing() {
// Basically just additional test setup
// I assume that someTestRecord/otherTestRecord are created/inserted before this
List<AggregateRateWrapper> testRates = new List<AggregateRateWrapper>();
AggregateRateWrapper wrappedRate = new AggregateRateWrapper();
wrappedRate.rate = 10;
wrappedRate.rateId = someTestRecord.Id;
wrappedRate.rateId2 = otherTestRecord.Id;
testRates.add(wrappedRate);
// Need to set the static var before calling .run()
SystemUnderTest.wrappedRates = testRates;
Test.startTest();
new SystemUnderTest().run();
Test.stopTest();
}
}
Id
, but I don't see a way to fake other props.