2

I'm having trouble figuring out why, when executing Test Class, which fires a Scheduled Class and the Batch Class within it, data I expect to be there, isn't.

@isTest static void test_scheduleExecution() {
  Test.startTest();
    Date tomorrow = Date.today().addDays(1);
    String day =  String.valueOf(tomorrow.day());

    if (day.length() == 1) {
      day = '0' + day;
    }

    String month = String.valueOf(tomorrow.month());

    if (month.length() == 1) {
      month = '0' + month;
    }

    String year = String.valueOf(tomorrow.year());
    String CRON_EXP = '0 0 0 ' + day + ' ' + month + ' ? ' + year + '';

    String jobId = System.schedule('ScheduleGetJobBoardActivity', CRON_EXP, new ScheduleGetJobBoardActivity());

    CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :jobId];

    System.assertEquals(0, ct.TimesTriggered);
    System.assertEquals('' + year + '-' + month + '-' + day + ' 00:00:00', String.valueOf(ct.NextFireTime));

    Job_Board_Activity__c[] activity = [SELECT Id FROM Job_Board_Activity__c];

    System.assertEquals(0, activity.size());
  Test.stopTest();

  activity = [SELECT Id FROM Job_Board_Activity__c];

  System.assertEquals(3, activity.size());
}

Long story short with this method: I'm scheduling the job, checking nothing's happened with it (e.g. it's not firing early and creating records before it should), but after Test.stopTest(); I'd expect the job to actually fire and there should be 3 records within the Job_Board_Activity__c object.

I know the Batch class is running correctly, because the finish() method is being hit and the for loop is looping through those records, but it isn't executing the INSERT request:

enter image description here

I checked the debug log and confirmed there are items in the List<>, so why wouldn't they be inserted?

Note: The scheduled class looks at records in Metadata types, and execute multiple batch classes depending on how many are in there, the Scheduled class is below:

global class ScheduleGetJobBoardActivity implements Schedulable {
  global void execute(SchedulableContext sc) {
    for (Job_Board_Activity_Setting__mdt activity : [SELECT Id, Label, Object_API_Name__c, Source_Relationship__c FROM Job_Board_Activity_Setting__mdt WHERE Active__c = true]) {
      BatchGetJobBoardActivity b = new BatchGetJobBoardActivity(activity.Object_API_Name__c, activity.Source_Relationship__c);
      Database.executeBatch(b);
    }
  }
}

Each iteration should insert 1 record over 3 loops, so 3 records in total.

Note 2: Coverage is fine on this, 100% on both classes. I'm just wondering why the asserts won't work?

Edit: The only thing I can think of now is that because the Batch class executes within a loop, the last loop doesn't have anything to insert and that's what the test class 'remembers'? But this is just a guess. I'd have though it would only 'forget' things outside of the Unit Test's method?

Edit 2:

Batch Class added for clarity:

global class BatchGetJobBoardActivity implements Database.Batchable<sObject>, Database.Stateful {
  global String objectAPIName;
  global String fieldAPIName;
  global static final Date DEFAULT_DATE = Date.today().addDays(-1);
  global Datetime dateToProcess;
  String query;
  global Map<Id, List<Id>> sourceToListOfRecords = new Map<Id, List<Id>>();

  global BatchGetJobBoardActivity(String obj, String fld) {
    this.objectAPIName = obj;
    this.fieldAPIName = fld;
    this.dateToProcess = DEFAULT_DATE;
  }

  global BatchGetJobBoardActivity(String obj, String fld, Date d) {
    this.objectAPIName = obj;
    this.fieldAPIName = fld;
    this.dateToProcess = d;
  }

  global Database.QueryLocator start(Database.BatchableContext BC) {
    query = 'SELECT Id FROM ' + objectAPIName + ' WHERE CreatedDate > ' + dateToProcess.format('yyyy-MM-dd') + 'T00:00:00Z AND CreatedDate < ' + dateToProcess.format('yyyy-MM-dd') + 'T23:59:59Z';
    return Database.getQueryLocator(query);
  }

  global void execute(Database.BatchableContext BC, List<sObject> scope) {
    String queryRecords;

    queryRecords = 'SELECT Id, ' + fieldAPIName + ' FROM ' + objectAPIName + ' WHERE Id IN :scope AND ' + fieldAPIName + ' != null';

    for (sObject sObj : Database.query(queryRecords)) {
      List<Id> l = new List<Id>();

      String[] fieldAPINameParse = fieldAPIName.split('\\.');

      /*
        Start: Tidy this part up!
      */

      if (fieldAPINameParse.size() == 1) {
        l = sourceToListOfRecords.get((Id)sObj.get(fieldAPINameParse[0]));
      }
      else if (fieldAPINameParse.size() == 2) {
        l = sourceToListOfRecords.get((Id)sObj.getSObject(fieldAPINameParse[0]).get(fieldAPINameParse[1]));
      }
      else if (fieldAPINameParse.size() == 3) {
        l = sourceToListOfRecords.get((Id)sObj.getSObject(fieldAPINameParse[0]).getSObject(fieldAPINameParse[1]).get(fieldAPINameParse[2]));
      }

      if (l == null && fieldAPINameParse.size() == 1) {
        l = new List<Id>();
        sourceToListOfRecords.put((Id)sObj.get(fieldAPIName), l);
      }
      else if (l == null && fieldAPINameParse.size() == 2) {
        l = new List<Id>();
        sourceToListOfRecords.put((Id)sObj.getSObject(fieldAPINameParse[0]).get(fieldAPINameParse[1]), l);
      }
      else if (l == null && fieldAPINameParse.size() == 3) {
        l = new List<Id>();
        sourceToListOfRecords.put((Id)sObj.getSObject(fieldAPINameParse[0]).getSObject(fieldAPINameParse[1]).get(fieldAPINameParse[2]), l);
      }

      /*
        Finish: Tidy this part up!
      */

      l.add(sObj.Id);
    }
  }

  global void finish(Database.BatchableContext BC) {
    Date batchDate = Date.valueOf(dateToProcess);
    List<Job_Board_Activity__c> jList = new List<Job_Board_Activity__c>();

    for (Id source : sourceToListOfRecords.keySet()) {
      Job_Board_Activity__c j = new Job_Board_Activity__c(
        Count__c = sourceToListOfRecords.get(source).size(),
        Date__c = batchDate,
        Object_Name__c = objectAPIName,
        Source__c = source
      );

      jList.add(j);
    }

    INSERT jList;
  }
}

Edit 3: So I wrote another method which manually executes the Batch method instead of relying on the Scheduler:

Test.startTest();
  BatchGetJobBoardActivity b = new BatchGetJobBoardActivity('ts2__Application__c', 'ts2__Application_Source__c', Date.today().addDays(-1));
  Database.executeBatch(b);
Test.stopTest();

Integer i = [SELECT COUNT() FROM Job_Board_Activity__c];

System.assertEquals(1, i);

The parameters are:

  • Object API name
  • Field API name
  • 'Collect records created on this date'

This works, the object and field API names are all that's stored in the MetaData which is what the scheduled class would iterate over, passing them in as Strings. I can only assume the problem is with the Scheduled class and not the Batch class as a result.

8
  • Add system debug statements to where you're building sourceToListOfRecords. If it's size is zero, you won't have any records to insert for jList. Can add the same system.debug statments to that for loop in the finish method as well. I don't see a SeeAllData, so not certain if you can execute that query without importing test data.
    – crmprogdev
    Jun 17, 2016 at 11:59
  • Thanks @crmprogdev I added System.debug('List size: ' + jList.size()); in the global void finish() method and saw that all the MetaData types are executing. There are 8 in total, five returned 0, three returned 1. This is what I expected. I have put in test data in a @testSetup method and not used seeAllData. It seems to know there's 1 record to insert, but it's not actually doing it?
    – Dan Jones
    Jun 17, 2016 at 12:04
  • Are you trying to insert database 'records' or 'metadata records' here?
    – crmprogdev
    Jun 17, 2016 at 12:13
  • @crmprogdev Database records, as I understand it MetaData records can't be manipulated with DML, only readable like profiles. I've editted my question with the actual Batch class. What's happening is that each metaData record contains the API name of an object and the API name of a field related to another 'central object'. It's just looping through those and counting the number of records.
    – Dan Jones
    Jun 17, 2016 at 12:16
  • You're correct, Apex can't insert metadata and that's why I asked the question. That was before you updated with the rest of your code. I'm not entirely certain how much of the Finish method can execute after Test.StopTest when it comes to DML. Hopefully @sfdcfox or someone else will drop in like cropredy and will spot the source of your issue.
    – crmprogdev
    Jun 17, 2016 at 12:19

1 Answer 1

2

Your problem may be that you are attempting to use Database.executeBatch() more than once in the testmethod:

Test.startTest();
  BatchGetJobBoardActivity b = new BatchGetJobBoardActivity('ts2__Application__c', 'ts2__Application_Source__c', Date.today().addDays(-1));
  Database.executeBatch(b);
Test.stopTest();

From the Apex developer doc:

When testing your batch Apex, you can test only one execution of the execute method. 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.

The executeBatch method starts an asynchronous process. When you test batch Apex, make certain that the asynchronously processed batch job is finished before testing against the results. Use the Test methods startTest and stopTest around the executeBatch method to ensure that it finishes before continuing your test. All asynchronous calls made after the startTest method are collected by the system. When stopTest is executed, all asynchronous processes are run synchronously. If you don’t include the executeBatch method within the startTest and stopTest methods, the batch job executes at the end of your test method. This execution order applies for Apex saved using API version 25.0 and later, but not for earlier versions.

So, by implication, the second, ... nth executeBatch() will do nothing within a test method

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .