I have a schedulable class that will run periodically, sending emails on certain conditions. I've tried to write a unit test using @testvisible private members, but some @testvisible members aren't being preserved.
Here's the basic outline:
global with sharing class VisitReminder implements Schedulable {
@TestVisible private Date runDate;
@TestVisible private List<Messaging.Email> reminders;
@TestVisible private List<Messaging.SendEmailResult> sendResults;
global VisitReminder() {
runDate = Date.today();
}
global void execute(SchedulableContext SC) {
reminders = new List<Messaging.Email>();
/* Code omitted that queries based on runDate,
* and adds Messaging.SingleEmailMessage() objects to reminders */
system.debug('Reminder count: ' + reminders.size()); // debug log shows reminders has members
sendResults = Messaging.sendEmail(reminders, false);
}
}
and the test class:
public class VisitReminderTest() {
/* code omitted that create test data */
VisitReminder vr = new VisitReminder();
vr.runDate = Date.parse('9/30/2013'); //special test date to work with test data
Test.startTest();
System.schedule('visitReminderTest', '0 0 19 * * ?', vrs);
Test.startTest();
system.assertEquals(Date.parse('9/30/2013'), vr.runDate); //this will pass
system.assertNotEquals(null, vr.reminders); //this will fail
system.assertNotEquals(null, vr.sendRestuls); //this also fails
}
The debug log shows that the execute() method ran, and created a list of emails (reminders). But when I try to inspect the list after test.stopTest(), it is now null. Likewise sendResults. The testvisible variable runDate preserves its value after Test.stopTest, however, it was set in the test method, so perhaps the vr variable in the test method is a copy of the pre-test version? Is @TestVisible incompatible with Schedulable? Or is this a more general Schedulable testing issue: the schedulable object's state isn't available after the asynchronous code has run? The method is intended to send emails, so I can't query changed records, and I'm having no luck tracking the emails as activities
Best Answer
The issue here is that the instance of VisitReminder constructed in your test is not the same instance that is instantiated by the platform Scheduler within the start and stop test scope. So your asserts are failing since the instance in the vr variable is never actually the executed instance.
The instance created by the Scheduler is created from a de-serialised copy of the one originally given to System.schedule by your test code. Unfortunately there is no way to access this instance or its state, its more than likely it is executed on another thread in the server and then destroyed.
The only state the test and the scheduled class share is the database and static variables. Added to the complication is that your only output of this is a set of emails, which do not get executed in a test anyway. Its interesting that the Salesforce example includes asserts in the schedule class.
So I see a few options...
Some of these solutions require changes to your schedule class solely for the benefit of your test, you can condition such code paths around a Test.isRunningTest() condition.
NOTE: Use of the 'global' modifier is optional these days (yet the Salesforce examples still include it). If your packaging this code this will be important to you, as it bakes in the class name and signature to the package. If your not packaging its not that big a deal. Eitherway you can use 'public' if you want.
An AssertCallback Solution
As per my summary of options above, this is inspired by KeithC's comment about using a logging approach to capture information to later assert, combined with an observation that asserts don't have to be executed in the immediate code path of the executing test.
First the usage, here is my Scheduled job.
And now my test...
There is a deliberate bug in the above to test the assertion fires and gives a meaningful stack trace.
Summary: As you can see this solution requires some instrumentation of the code your testing, which could in fact be used regardless of batch/schedule or not (a poormans Mockito). So yes it does have the overhead of calling a method in your code, which if you where really concerned about statements could be conditioned around Test.isTestRunning(). Though the AssertCallback.assert method does do this also. The main thing though is it has allowed you to assert anything you want, when you want.
Finally you can find the source code to the AssertCallback class here.
Hope this helps!