(Note that this blog post refers to functionality that is intended to be made available in the Spring 16 release of Salesforce, but features may be removed prior to the Spring 16 go-live date, so you may never see this in the wild!)
Introduction
Anyone who has written a reasonable amount of Apex unit tests is likely to have butted up against the shortcoming that the CreatedDate is set when a record is inserted into the Salesforce database. While at first glance this may not seem to be such a huge issue, consider the scenario of a custom Visualforce page, maybe for use in a dashboard, that displays the 10 most recent cases:
The controller is about as straightforward as it can be - simply querying back the records from the Salesforce database:
public class RecentCasesController { public List<Case> getRecentCases() { return [select id, CaseNumber, Subject, Account.Name, Contact.Name, CreatedDate from Case order by CreatedDate desc limit 10]; } }
while the page is equally straightforward, simply iterating the records in a pageblocktable:
<apex:page controller="RecentCasesController"> <apex:pageBlock title="10 Most Recent Cases"> <apex:pageBlockTable value="{!recentCases}" var="cs"> <apex:column value="{!cs.subject}"/> <apex:column value="{!cs.Account.Name}" /> <apex:column value="{!cs.Contact.Name}" /> <apex:column value="{!cs.CreatedDate}" /> </apex:pageBlockTable> </apex:pageBlock> </apex:page>
A Testing Problem
Testing this in the Winter 16 release (and earlier), testing this functionality meant a compromise, such as:
- Inserting a number of records and ensuring that only 10 of them were returned, but being unable to predict which 10 this would be. This is because the CreatedDate field has second granularity, and if there are a number of records inserted in the same second, there is no guarantee of the order they will be returned in when the query is ordered by the CreatedDate. The Salesforce database will do the least amount of work to execute the query, and there’s nothing you can do to affect it. From a unit testing perspective, unless you can 100% guarantee the order of the results, asserting for a particular order makes a test fragile.
- Adding an autonumber field to the case sobject so that you can rely on that to guarantee the ordering. This is a classic example of changing your implementation to satisfy your unit tests, which if nothing else is annoying to have to resort to.
- Trying to kill some time to allow the second to tick over in between record inserts, by looping a lot and generating debug statements for example. There are a couple of problems with this approach. First, it is fragile, as you have no control over how much time would be burned this way. All you can do is hope that the performance you have seen in the past continues, which can’t give much confidence. More importantly, it is evil, as you are consuming shared resources for no good reason.
- Provide some form of dependency injection, probably via a @TestVisible private property, to allow your test to inject the list of cases that should be returned as a result. This mechanism means that all you are really testing is that the controller returns the list of cases you asked it to, bypassing the query that will be executed in production. Writing code that solely allows your tests to complete successfully is usually an indication that something is awry.
One DOES Simply Set the CreatedDate
In the Spring 16 release this is all set to change (although it may not - see the note at the top of this blog). The new Test.setCreatedDate(Id, DateTime) method allows you to change the CreatedDate once a record is inserted.
For my recent cases page, I can now write a test case to verify that only the 10 most recent cases are returned, by setting the CreatedDate for each case record after I’ve inserted them in the database:
@isTest private class RecentCasesController_Test { @isTest static void TestGetRecentCases() { /* setup */ Account acc=new Account(Name='Unit Test'); insert acc; Contact cont=new Contact(FirstName='Unit', LastName='Tester'); insert cont; List<Case> cases=new List<Case>(); for (Integer idx=1; idx<=20; idx++) { Case cs=new Case(Subject='Test Case ' + idx, AccountId=acc.Id, ContactId=cont.Id); cases.add(cs); } insert cases; // now set the created date for each case - the first case // in the list will be the most recent, the second case the // second most recent, and so on for (Integer idx=1; idx<=20; idx++) { DateTime created=System.now().addDays(-(idx*10)); Test.setCreatedDate(cases[idx-1].Id, created); } /* execute */ RecentCasesController ctrl=new RecentCasesController(); List<Case> recentCases=ctrl.getRecentCases(); /* verify */ // should be 10 cases System.assertEquals(10, recentCases.size()); // should be the first 10 inserted, in that order for (Integer idx=0; idx<10; idx++) { System.assertEquals(recentCases[idx].Id, cases[idx].Id); } } }
Final Thoughts
The ability to set the created date is a much needed addition to the Salesforce platform testing framework, and one that will allow production code to be properly tested, and allow some workarounds/terrible hacks to be retired.
It would be cool to be able to set the CreatedDate when constructing the sObject, prior to insertion, but I’d imagine this would require a ton of changes to the Salesforce database layer and isn’t going to happen. I foresee thousands of test fixture classes that insert a record and then change its CreatedDate!
Its a shame that at the moment you can only set the CreatedDate of a single record per call, but hopefully we’ll get some bulk capability in the future. The Salesforce platform approach is typically to make things doably difficult, then add the bells and whistles.
Finally, the documentation doesn’t mention any effects on limits and my testing with the code above bears this out, which is always good.
Related Posts
Thanks Bob super helpful when testing processes based on record creation date.
ReplyDeleteThanks Bob super helpful when testing processes based on record creation date.
ReplyDeleteThis comment has been removed by a blog administrator.
ReplyDeleteGreat Post Keir. This will be super helpful functionality.
ReplyDelete