2

I'm currently working on testing some Salesforce Apex code that involves AggregateResult objects. In my test setup, I need to create a mock AggregateResult object with fields rateId and rateId2 set to specific values for testing purposes.

Schema.SObjectType aggregateResultType = Schema.getGlobalDescribe().get('AggregateResult');
SObject newAggregateResult = aggregateResultType.newSObject();
newAggregateResult.put('rateId', 'ID-001');
newAggregateResult.put('rateId2', 'ID-002');

However, when I run this code, I encounter the following exception:

System.SObjectException: Invalid field rateId for AggregateResult

I understand that AggregateResult objects are different from regular sObjects, and my approach might be incorrect. How can I properly create a mock AggregateResult object with custom fields for testing in Salesforce Apex? Any guidance or examples would be greatly appreciated.

PS: My query is returning this aggregate result:

enter image description here

1
  • 1
    The usual way to fake records is JSON.deserialize. However, it looks like AggregateResult has some internal "magic." I can't seem to get it to work, either. You can fake Id, but I don't see a way to fake other props.
    – sfdcfox
    Commented Jun 5 at 14:25

1 Answer 1

2

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

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .