4

Here I thought the new Analytics API actually allowed us to create reports from scratch using apex. Guess not.

I have a VF page and a controller extension that interacts with reports and their data to do various stuff. Salesforce's documentation on test methods involving reports seems to suggest the following as best practice:

  • Set your test method as (SeeAllData=True)
  • Use an actual existing report in your org for testing purposes
  • Make sure to include the report in all code deployments or it won't work!

This strikes me as contrary to what SF has pushed on us as best practice or necessary methodology for testing code. Best practice dictates that (SeeAllData) is a crutch, and that test methods should be as self-contained as possible. Instead now I have to remember to push this report, and I now have a report in production labeled "DO NOT DELETE" that end users ask me about.

Can I get confirmation that this is indeed the best way to go about testing code around reports? I feel like I'm missing something but I can't find an alternative method.

  • 1
    If you are using SeeAllData anyway, you could use a Custom Setting to manage which Report(s) to use. – Adrian Larson Oct 21 '15 at 16:35
1

There's an alternate (somewhat more work) approach if you want to avoid SeeAllData=true but will require a different mindset to how you code the controller

  • Don't use the built-in Apex classes in the Reports namespace
  • Instead, use a callout to the Analytics API - wherein you are calling back in to the same org with the same session id as the running user.
  • Decode the factmap (JSON) using your own logic and the SFDC Json deserialization methods
  • In your testmethod, because report data is being fetched via a callout, you can mock the response and thus don't need any org data or preexisting report

Personally, I'd probably do what you did with the 'do not delete' report as you can leverage the built in classes and methods in the Reports namespace.

Note you can always put the do not delete report in a "Must Retain For Deployments" folder no one has access to except sysads

  • Gonna mark this as the answer because you present a way to at least mock creating test data, which avoids creating a 'real data' report. But I agree, it's probably just easier to create a report than to mock callout responses from the Analytics API to simulate report data. – smohyee Oct 22 '15 at 19:47
2

If you are using SeeAllData anyway, you could use a List Custom Setting to manage which Report(s) to use.

Example:

Custom Report
Name               Report Name    Report DeveloperName    Other Information
00OX00000011111    My Report 1    My_Report_1             Data point 1
00OX00000011112    My Report 2    My_Report_2             Data point 2
  • Doesn't address the question of whether there's a better way than (SeeAllData=True) + premade report, but I agree that custom setting feature might make it easier to package for deployment to different orgs. – smohyee Oct 22 '15 at 19:48
1

EDIT - I had some trouble deserializing the JSON due to nulls etc.

To get around this, you can remove the factMap and make sure that other data types are correct like so:

String jsonReport = '{"hasDetailRows":true,"groupingsDown":{"groupings":[]},"groupingsAcross":{"groupings":[]},"allData":true, "reportMetadata":{"topRows":{},"standardFilters":[],"standardDateFilter":{"startDate":null,"endDate":null,"durationValue":"CUSTOM","column":"CREATED_DATE"},"sortBy":[],"showSubtotals":true,"showGrandTotal":true,"scope":"user","reportType":{"type":"AccountList","label":"Accounts"},"reportFormat":"TABULAR","reportFilters":[],"reportBooleanFilter":"","name":"Dummy","id":"00O3B000000Jl8xUAC","historicalSnapshotDates":[],"hasRecordCount":true,"hasDetailRows":true,"groupingsDown":[],"groupingsAcross":[],"division":"","developerName":"Dummy","detailColumns":["TYPE"],"description":"","customSummaryFormula":{},"currencyCode":"USD","crossFilters":[],"buckets":[],"aggregates":["RowCount"]}}';
Reports.ReportResults results = (Reports.ReportResults)JSON.deserialize(jsonReport, Reports.ReportResults.class);

Then deserialize the factMap separately:

String reportFacts = '{"T!T":{"key":"T!T","aggregates":[{"value":4,"label":"4"}],"rows":[{"dataCells":[{"value":"test1","label":"test1"}]},{"dataCells":[{"value":"test2","label":"test2"}]},{"dataCells":[{"value":"test3","label":"test3"}]},{"dataCells":[{"value":"test4","label":"test4"}]}]}}';
Reports.ReportFactWithDetails factMapWithDetails = (Reports.ReportFactWithDetails)JSON.deserialize(reportMetaExtend, Reports.ReportFactWithDetails.class);

At this point, you can't get the factmap back into the main report object, so perhaps the best way to deal with this is to use the separate pieces (metadata, extendedMetadata, factmap) separately, both in Testing and general usage.

If anyone has any luck deserializing the entire report object, please post your results as a comment here.


I just ran into this annoying issue - however, I think there is a better way. Instead of having an actual report having to be around and having SeeAllData=True,

Use JSON deserialization to help!

In your test class, define a string containing the report that you want to generate (ie your mock), and then just deserialize it like so:

//define JSON
private static String dummyReportString = '{"reportMetadata":{"topRows":null,"standardFilters":null,"standardDateFilter":{"startDate":"2016-10-30","endDate":null,"durationValue":"CUSTOM","column":"CREATED_DATE"},"sortBy":null,"showSubtotals":true,"showGrandTotal":true,"scope":"user","reportType":{"type":"AccountList","label":"Accounts"},"reportFormat":"TABULAR","reportFilters":null,"reportBooleanFilter":null,"name":"Dummy","id":"00O3B000000Jl8xZOO","historicalSnapshotDates":null,"hasRecordCount":true,"hasDetailRows":true,"groupingsDown":null,"groupingsAcross":null,"division":null,"developerName":"Dummy","detailColumns":["USERS.NAME"],"description":null,"customSummaryFormula":null,"currencyCode":null,"crossFilters":null,"buckets":null,"aggregates":["RowCount"]},"reportExtendedMetadata":{"groupingColumnInfo":null,"detailColumnInfo":{"USERS.NAME":{"name":"USERS.NAME","label":"Account Owner","dataType":"STRING_DATA"}},"aggregateColumnInfo":{"RowCount":{"name":"RowCount","label":"Record Count","downGroupingContext":null,"dataType":"INT_DATA","acrossGroupingContext":null}}},"hasDetailRows":true,"groupingsDown":{"groupings":null},"groupingsAcross":{"groupings":null},"factMap":{"T!T":{"key":"T!T","aggregates":[{"value":4,"label":"4"}],"rows":[{"dataCells":[{"value":"005i00000080XvdAHC","label":"Some Dude"}]},{"dataCells":[{"value":"005i00000080XvdAUO","label":"Some Dude"}]},{"dataCells":[{"value":"005i00000080XvdAGC","label":"Some Dude"}]},{"dataCells":[{"value":"005i00000080XvdAEK","label":"Some Dude"}]}]}},"allData":true}';

//deserialize:
Reports.ReportResults results = (Reports.ReportResults)JSON.deserialize(dummyReportString, Reports.ReportResults.class);

Then just use as a normal report. I have used mine in the report retrieval class so that when called, it always returns a report (a normal one in normal circumstances or if Test.isTest() == true - the fake one.

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.