4

I have a process that needs to run once daily. It will first need to make a series of callouts to get information (sms email address) to update contacts, and it will then need to send emails using the updated contacts. The callouts may exceed 10, so I have designed a Batchable class that can find all the correct records, make the callouts in batches, and update the records. From the finish() method I want to invoke the method to send the emails.

Here's the rough outline:

global class Reminders_Schedulable implements Schedulable {

    global void execute() {
        // OMITTED: do a little setup work 

        // Run the batchable to make callouts and update contact's SMS email addresses
        Database.executeBatch(new SMSGatewayUtil_Batchable(), SMSGatewayUtil.MAX_BATCH_SIZE);
    }

    global static void SendReminders() {
        // send the actual reminders using the updated contacts
    }
}

global class SMSGatewayUtil_Batchable 
             implements Database.Batchable<SObject>, Database.AllowsCallouts {

    global Database.QueryLocator start(Database.BatchableContext bc) {
         system.debug('&&&& SMSGatewayUtil_Batchable.start');
         return Database.getQueryLocator(...);
    }

    global void execute(Database.BatchableContext bc, list<Contact> contacts) {
         system.debug('&&&& SMSGatewayUtil_Batchable.execute');
         SMSGatewayUtil.UpdateContactsSMSEmail(contacts);
         update contacts;
    }

    global void finish(Database.BatchableContext bc) {
         system.debug('&&&& SMSGatewayUtil_Batchable.finish');
         Reminders_Schedulable.SendReminders();
    }

}

So the scheduleable class's execute() method runs the batchable, which updates makes callouts and updates contacts; finally the batchable's finish() method calls a static method to send the emails, using the updated contacts. Is there are reason why the schedulable's execute() method shouldn't be able to call the batchable()? I don't think there is, but thought I'd check.

The bigger question - is this testable as is? I have a test method that calls system.schedule to run the schedulable (in between startTest and stopTest). The execute() method is called, and I see the SMSGatewayUtil_Batchable constructor invocation on the executeBatch() line in the log, but the batchable doesn't appear to run - the debug statements in the batchable class do not even appear in the log. There's also no error messages.

Update: As I failed to mention originally, my test class is setting up a mock interface for the callout. The question that I have is that the Database.Batchable is being constructed but not invoked - as shown in my outline above, each method in the Batchable has a debug statement, and none of them are showing up in the debug log - not even the start() method.

4

Yes, you can call batchable classes from schedulable classes. This is a very common use case.

As for testing you'll want to test them separately. Test methods for my schedulable classes that invoke a batchable class just verify that the batchable class is queued after the scheduled class has executed. I then have a separate test methods for my batch class that is queued for the test directly rather than through the schedulable class.

The reason behind this is the Test.stopTest() method only clears out queues (future, batch, scheduled) once. So you can have multiple future and batch calls run at that point, but if those cause any further queuing of processes they aren't run. Currently there isn't a way to flush the queues twice, just need to test them separately, which personnally I feel is a better and clearer testing scheme since it separates the various roles of each class better.

  • 1
    "Test.stopTest() method only clears out queues (future, batch, scheduled) once" - aha, this makes sense. I just replaced my test method's System.schedule call with a direct call to the class's execute() method, and everything works. Which is fine... I don't need to test the scheduler, just my execute method. – Jason Clark Nov 26 '13 at 21:38
  • That statement is not true. The code I posted proves otherwise. – sfdcfox Nov 26 '13 at 22:01
6

Yes, the normal design of a scheduled class is to kick off a batch process. It's also possible to combine the scheduler and batch into a single class, using the following design:

public class MyProcessor implements Schedulable, Database.Batchable<SObject> {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([select id from account]);
    }
    public void execute(Database.BatchableContext bc, Account[] records) {
        System.debug('*** Do Something Here ***');
    }
    public void finish(Database.BatchableContext bc) {
        System.debug('*** Do Something Here ***');
    }
    public void execute(SchedulableContext sc) {
        Database.executeBatch(this);
    }
}

Note that execute won't fire if there is nothing to do. Here's an example test.

@isTest
class MyProcessorTest implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest req) {
        HttpResponse res = new HttpResponse();
        res.setStatusCode(401);  // We demo "unauthorized" here
        return res;
    }
    @isTest
    static void test() {
        Test.setMock(HttpCalloutMock.class, new MyProcessorTest());
        MyProcessor mp = new MyProcessor();
        Account a = new Account(Name='Test');
        insert a;
        System.schedule('Test', '0 0 * ? * * *', mp);
        Test.startTest();
        Test.stopTest();
    }
}

Edit: Also, I should mention that callouts need to use the mock callout interface, because you can't actually perform a callout during a test (a protection against updating a live system with dummy data). I've updated the test to reflect how you can use this interface.

Edit 2: So, batch callouts are not supported in testing, but calling a batch from a scheduled call is. Thus, it prompted me to write the following code (with 100% coverage):

public class MyProcessor implements Schedulable, Database.Batchable<SObject>, Database.AllowsCallouts {
    @TestVisible execute mode;

    public MyProcessor() {
        Map<Boolean, Type> modes = new Map<Boolean, Type>();
        modes.put(test.isRunningTest(), testmode.class);
        modes.put(!test.isrunningtest(), livemode.class);
        mode = (execute)modes.get(true).newinstance();
    }

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([select id from account]);
    }

    public interface execute {
        void execute(Account[] records);
    }

    public class testMode implements execute {
        public void execute(Account[] records) {
        }
    }
    public class liveMode implements execute {
        public void execute(Account[] records) {
            Http http = new Http();
            HttpRequest req = new HttpRequest();
            req.setMethod('GET');
            req.setEndpoint('https://www.google.com/');
            HttpResponse res = http.send(req);
        }
    }            
    public void execute(Database.BatchableContext bc, Account[] records) {
        mode.execute(records);
        System.debug('*** Do Something Here ***');
    }
    public void finish(Database.BatchableContext bc) {
        System.debug('*** Do Something Here ***');
    }
    public void execute(SchedulableContext sc) {
        Database.executeBatch(new MyProcessor());
    }
}

Test class:

@isTest
public class MyProcessorTest implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest req) {
    HttpResponse res = new HttpResponse();
        res.setStatusCode(200);
        res.setStatus('Found');
        res.setHeader('Content-Type', 'text/plain');
        res.setBody('Hello World!');
        return res;
    }
    @isTest static void test1() {
        MyProcessor mp = new MyProcessor(); 
        Account record = new Account(Name='Test');
        insert record;
        Test.startTest();
        System.schedule('Test', '0 0 * ? * * *', mp);
        Test.stopTest();
    }
    @isTest static void test2() {
        MyProcessor mp = new MyProcessor();
        Test.startTest();
        Test.setMock(HttpCalloutMock.class, new MyProcessorTest());
        mp.mode = new MyProcessor.livemode();
        mp.mode.execute([SELECT Id, Name FROM Account]);
        Test.stoptest();
    }
}
  • "Note that execute won't fire if there is nothing to do" - it's true that the Batchable interface's execute won't fire if start doesn't return any records. However, my start method immediately writes a debug message, and that message is not appearing in the log. As noted in the question, I see the Batchable constructor being invoked (in the ExecuteBatch statement), but no debug messages from inside the batchable appear. I suspect that the testing framework doesn't support a call to Batchable.execute inside a Schedulable.execute implementation, but I haven't found confirmation in the docs. – Jason Clark Nov 26 '13 at 21:26
  • And yes, my test method calls SetMock, but the code that makes the callout is inside the Batchable, and is never invoked. – Jason Clark Nov 26 '13 at 21:27
  • Accepted @Ralph's answer as it had the testing info I needed, but +1 here for good sample code and explanation. – Jason Clark Nov 26 '13 at 21:42
  • @JasonClark Interesting, but this example code runs in my organization-- and achieves 100% coverage. Perhaps there's something else going on? More to the point, I don't think that answer is correct, since I am able to get 100% coverage with my example code. – sfdcfox Nov 26 '13 at 21:44
  • 1
    This answer suggests that I've run face-first into a bug regarding callouts in batch apex code. That's unfortunate. – sfdcfox Nov 26 '13 at 22:37

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.