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...
- Place the Asserts in the Schedule Class. Its interesting that the Salesfore example places asserts in the schedule class. So I thought I'd try it. Indeed this works, and of course you can condition the execution of the System.assert calls around a Test.isRunningTest() check. This is probably your cheapest option, but is also a little ugly to see in production code and not in test code.
- Assert Callbacks. Inspired by KeithC's comment on the other answer and the above observation, I've developed a generic solution to allow you to express the asserts in your test code, yet have them invoked during the execution of the scheduled job with the correct state available. See below for more details on this approach.
- Use of Static Variables. You can create a static @TestVisible variable in your VisitReminder class, e.g. testReminders and testSendResults, then in the last statements of the execute assign these from the instance versions. Your test will be able to see these via VisitReminder.testReminders etc. Personally I feel this is leveraging an implementation detail of the platform, that said I've seen it used elsewhere so likely now one of those approaches that will be here to stay.
- Use of a Reminder History Object.. You may want to consider that having a history of what reminders have been sent for future reference is a good feature to give to your users. Thus if so a new object will also help with your testing, as records emitted by the scheduled class can readily be asserted via SOQL in the test code.
- Email.setSaveAsActivity. You may in fact want to look at using setSaveAsActivity as this saves email activity to the target object of the email (if you have an appropriate one). This might in fact be a good feature anyway for you to review when reminders where sent historically.
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.
public with sharing class MyScheduledWork implements Schedulable {
@TestVisible
private String message;
public void execute(SchedulableContext SC)
{
message = 'Goodbye X';
AssertCallback.assert(MyScheduledWork.class, 'execute', this);
}
}
And now my test...
@IsTest
private with sharing class MyScheduledWorkTest {
@IsTest
private static void testSchedule()
{
// Register assert callback
AssertCallback.registerCallback(MyScheduledWork.class, new AssertMyScheduleWork());
// Run the scheduled job
MyScheduledWork testSchedule = new MyScheduledWork();
testSchedule.message = 'Hello';
Test.startTest();
System.schedule('Test', '0 0 19 * * ?', testSchedule);
Test.stopTest();
}
public class AssertMyScheduleWork implements AssertCallback.IAssertCallback
{
public void assert(String location, Object state)
{
// Assert callback from MyScheduledWork?
if(state instanceof MyScheduledWork)
{
// State to assert
MyScheduledWork myScheduledWork = (MyScheduledWork) state;
// Assert the state of the scheduled job
System.assertEquals('Goodbye', myScheduledWork.message);
}
}
}
}
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!
Here's the problem from the docs:
When testing your batch Apex, you can test only one execution of the execute method. You can use the scope parameter of the executeBatch method to limit the number of records passed into the execute method to ensure that you aren't running into governor limits.
Additionally I don't think batch classes are executed from schedulable classes in test classes but I couldn't find that supporting documentation. From my own experience, testing schedulable batch methods only queues the batches while testing the batch method execution actually runs the batch method. So the asserts after your schedulable tests will always fail if they expect a batch job to complete.
Basically, this means you should test the batch and schedulable classes separately. Once you remove the asserts from the schedulable tests cases everything should work fine and it looks like you'll have full coverage.
Also, you only need one '@isTest' line at the top of your test class. I'm also assuming that the schedulable method at the botton of the test class is not a part of that test class but just a mockup of a standalone class used during production. Just in case, here are how the separate classes should be written:
Test Class
@isTest
private class ScheduleBatchTest {
private enum TestCase {
DatabaseExecuteBatch,
ScheduleBatch,
Schedule
}
static void test1() {
execute(TestCase.DatabaseExecuteBatch);
}
static void test2() {
execute(TestCase.ScheduleBatch);
}
static void test3() {
execute(TestCase.Schedule);
}
private static void execute(TestCase tc) {
Account[] accounts = new Account[] {
new Account(Name = 'Acme'),
new Account(Name = 'Nike')
};
insert accounts;
Test.startTest();
if (tc == TestCase.DatabaseExecuteBatch) {
Database.executeBatch(new BatchableExecutorTestBatchable());
Account[] actuals = [select Site from Account where Id in :accounts];
System.assertEquals(accounts.size(), actuals.size());
for (Account actual : actuals) {
System.assertEquals('executed', actual.Site);
}
} else if (tc == TestCase.ScheduleBatch) {
System.scheduleBatch(new BatchableExecutorTestBatchable(),
'my description', 5, 100);
//assert batch job queued here
} else if (tc == TestCase.Schedule) {
System.schedule('my job', '0 0 13 * * ?', new ScheduledBatchable());
//assert scheduled job queued here
}
Test.stopTest();
}
Batch Class:
public class BatchableExecutorTestBatchable implements Database.Batchable<SObject>, Database.Stateful {
public Database.QueryLocator start(Database.BatchableContext bc) {
Database.QueryLocator ql = Database.getQueryLocator([
select Name from Account order by Name
]);
insert new Account(Name = 'start');
return ql;
}
public void execute(Database.BatchableContext bc, List<SObject> scope) {
for (SObject sob : scope) {
Account a = (Account) sob;
a.Site = 'executed';
}
update scope;
insert new Account(Name = 'execute');
}
public void finish(Database.BatchableContext bc) {
insert new Account(Name = 'finish');
}
}
Schedulable Class:
private class ScheduledBatchable implements Schedulable {
public void execute(SchedulableContext sc) {
Database.executeBatch(new BatchableExecutorTestBatchable(), 100);
}
}
Let me know if this works.
Best Answer
Ive had a play with this in a dev instance and it seems changing your second functions arguments from
to
fixes the error message.
EDIT: Ive now tested this in a dev instance and substituting 'Object' for 'Schedulable' works and the job does get created.
This blog also seems to be trying to achieve something similar to you: http://alexdberg.blogspot.co.uk/2011/06/starting-apex-scheduled-jobs-without.html