9

I'm trying to write most of my tests without hitting salesforce database. For that I'm implementing some simple mock class where I just return some SOQL queries when needed. The problem I'm facing now is with AggregateResult. Was someone able to create in memory this so that I can returned it in one of my mocked methods?

The query I want to mock in this particular case is: [SELECT MIN(CreatedDate) minCaseCreateDate FROM Case WHERE Id in :caseIds]

3
  • Kinda depends on what you are trying to do with them.
    – Adrian Larson
    Commented May 5, 2017 at 20:33
  • I'm just interested in why you would want to avoid hitting the database in your test. Commented May 5, 2017 at 21:14
  • 2
    @rael_kid I've mocked all sorts of records, simply because it's a great way to unit test individual methods instead of end-to-end tests.
    – sfdcfox
    Commented May 5, 2017 at 21:16

2 Answers 2

10

Check edit history for my initial approach. After revisiting this question and with a bit more experience, I would pursue this path:

public class Aggregate
{
    public static List<Aggregate> query(List<AggregateResult> results)
    {
        return service.convert(results);
    }

    final Map<String, Object> data;
    public Aggregate(AggregateResult result)
    {
        data = result.getPopulatedFieldsAsMap();
    }
    public Object get(String alias) { return data.get(alias); }

    @TestVisible Aggregate() { data = new Map<String, Object>(); }
    @TestVisible Aggregate put(String alias, Object value)
    {
        data.put(alias, value);
        return this;
    }

    static Service service = new Service();
    @TestVisible static void setMock(Service mock) { service = mock; }
    @TestVisible virtual class Service
    {
        protected virtual List<Aggregate> convert(List<AggregateResult> results)
        {
            List<Aggregate> aggregates = new List<Aggregate>();
            for (AggregateResult result : results)
                aggregates.add(new Aggregate(result));
            return aggregates;
        }
    }
}

I was able to reference this query quite similar to how it would normally look:

List<Aggregate> aggregates = Aggregate.query([
    SELECT ProfileId, count(Id) records
    FROM User GROUP BY ProfileId
]);
for (Aggregate aggregate : aggregates)
{
    system.debug(aggregate.get('ProfileId') + ' - ' + aggregate.get('records'));
}

And it would be quite easy to mock:

@IsTest
class AggregateTests
{
    class Mock extends Aggregate.Service
    {
        final List<Aggregate> aggregates = new List<Aggregate>();
        public Mock add(Aggregate aggregate)
        {
            aggregates.add(aggregate);
            return this;
        }
        public override List<Aggregate> convert(List<AggregateResult> results)
        {
            return aggregates;
        }
    }
    @IsTest static void testMocking()
    {
        Id dummyId = SObjectType.Profile.getKeyPrefix().rightPad(15, '0');

        Mock fakeData = new Mock().add(new Aggregate()
            .put('ProfileId', dummyId)
            .put('records', 42));

        Test.startTest();
            Aggregate.setMock(fakeData);
            List<Aggregate> aggregates = Aggregate.query([
                SELECT ProfileId, count(Id) records
                FROM User GROUP BY ProfileId
            ]);
        Test.stopTest();

        system.assertEquals(1, aggregates.size(), 'Only mock data should be returned');
        system.assertEquals(dummyId, aggregates[0].get('ProfileId'), 'Only mock data should be returned');
        system.assertEquals(42, aggregates[0].get('records'), 'Only mock data should be returned');
    }
}
13
  • 1
    can you please check how it will work this line Proxy aggregateProxy = new AggregateProxy(); is it typo error? Commented May 5, 2017 at 22:37
  • 1
    When trying to do this: AggregateResult agg = (AggregateResult)JSON.deserialize('{"minCaseCreateDate":"2017-05-08T14:56:14.000+0000"}', AggregateResult.class); I'm getting the error: System.JSONException: No such column 'minCaseCreateDate' on sobject of type AggregateResult It seems that you can't just put the column name that you want :( Commented May 8, 2017 at 15:36
  • 1
    I even tryed with your JSON string and having the same error. EDIT: The error appears at runtime Commented May 8, 2017 at 16:11
  • 2
    Perhaps I'm doing something wrong, but all the attempts I've made to serialize an AggregateResult, including actually getting one from a query and doing round-trip, have resulted in the resulting object not having any attributes. Serialization seems to work just fine, but the system doesn't seem to like deserialization. Commented May 8, 2017 at 19:01
  • 1
    Adrian -- I don't think this works - I tried at various versions V46 down to V36 and in all cases, the deserialized object is empty. Roundtrip from a real query to serialize to deserialize also yields empty AggregateResult objects. At the moment, I think one needs to create an AggregateResultProxy that can be mocked by testmethods
    – cropredy
    Commented Aug 14, 2019 at 19:28
5

I so much wanted @AdrianLarson original Json.deserialize answer to be right as it is easy to fabricate but while it works at compile time, at run time, the deserialized AggregateResult is empty ( debug displays as {} ).

So, here's what I did:

Create an AggregateResultProxy object

/**
 * AggregateResult can't be mocked so wrap with AggregateResultProxy
 * which can be mocked by test methods. 
 */

public class AggregateResultProxy {

    Map<String,Object> valuesByAlias = new Map<String,Object> ();

    public AggregateResultProxy withAggregateResult(AggregateResult groupedResult) {
        String groupedResultJson = JSON.serialize(groupedResult);
        this.valuesByAlias = (Map<String,Object>) JSON.deserializeUntyped(groupedResultJson);
        return this;
    }
    public AggregateResultProxy withAliasAndValue(String alias, Object val) {
        this.valuesByAlias.put(alias,val);
        return this;
    }

    public Map<String,Object> getValuesByAlias() {return this.valuesByAlias;}
}

My selector method

return not a List<AggregateResult> but instead a List<AggregateResultProxy>

AggregateResultProxy[] mySelectorMethod(...) {
   AggregateResultProxy[] groupedResultProxies = new List<AggregateResultProxy>();
   for (AggregateResult groupedResult: 
         [SELECT COUNT(ID) RECORDCOUNT, Status STATUS
            FROM Asset 
            WHERE ...
            GROUP BY Status ] ) {
    groupedResultProxies.add(
      new AggregateResultProxy().withAggregateResult(groupedResult));
  }
  return groupedResultProxies;
}

My runtime code

public void doStuff() {
  for (AggregateResultProxy groupedResultProxies :AssetsSelector.newInstance().mySelectorMethod() ) {
    Map<String,Object> groupedResultProxy = groupedResultProxies.getValuesByAlias();
    System.debug('Status = ' + groupedResultProxy.get('STATUS'));
    System.debug('Count = ' + groupedResultProxy.get('RECORDCOUNT')); 
  }
}

which might display

Status = Active
Count = 23
Status = Canceled
Count = 6 

and my testmethod

using ApexMocks (you could of course use your own favorite dependency injection approach)

fflib_ApexMocks mocks = new fflib_ApexMocks();
// given mocked AggregateResults
AggregateResultProxy[] mockedGroupedResults = new List<AggregateResultProxy> {
   new AggregateResultProxy()
     .withAliasAndValue('STATUS','Active')
     .withAliasAndValue('RECORDCOUNT', 2),
   new AggregateResultProxy()
     .withAliasAndValue('STATUS','Cancelled')
     .withAliasAndValue('RECORDCOUNT', 7)
}; 
// Given mock selector (stubbed return val)
AssetsSelector mocksSelector = (AssetsSelector) mocks.mock(AssetsSelector.class);
mocks.startStubbing();
mocks.when(mocksSelector.sObjectType()).thenReturn(Asset.SObjectType);
mocks.when(mocksSelector.mySelector(...))
                .thenReturn(mockedGroupedResults);
mocks.stopStubbing();
// Given mocks injected
Application.Selector.setMock(mocksSelector);

// When myMethodUnderTestIsCalled
myObject.doStuff();
// Then verify ...
...asserts or apexMocks verifies go here

You must log in to answer this question.

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