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
    May 5, 2017 at 20:33
  • I'm just interested in why you would want to avoid hitting the database in your test. 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
    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? 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 :( 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 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. 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
    Aug 14, 2019 at 19:28
4

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.