Direkt zum Hauptbereich

Batch - Aufbau

Hin und wieder kommt es vor, dass viele Daten im Hintergrund aktualisiert werden müssen. Es ist keine Zauberei, einen Batch dafür zu schreiben. Viel mehr kommt es auf die Technik an. Schließlich muss jeder Batch auch getestet werden. Da Batches asynchron laufen, also zeitversetzt, und nicht unmittelbar nach dem Start, wird die Überprüfung der Testergebnisse nicht einfach sein.
Daher ist es empfehlenswert, die komplette Logik in eine weitere Klasse auszulagern. So können die einzelnen Methoden und Funktionen dieser Klasse sowohl vom Batch benutzt, als auch im Testlauf geprüft werden.

Schematisch dargestellt sieht der Aufbau wie folgt aus:


Batch - Löschen von Daten (f42_Batch_DeleteData)
global class f42_Batch_DeleteData implements Database.Stateful, Database.Batchable<SObject>, Schedulable{
public f42_Batch_DeleteDataHelper batchHelper;
public String soql;

 /* Constructor */
 /*********************/
 public f42_Batch_DeleteData(){
  batchHelper = new f42_Batch_DeleteDataHelper();
 }

 /* execute SchedulableContext */
 /******************************/
 global void execute(SchedulableContext SC) {
  for(String s : batchHelper.buildSearchStrings()){
   f42_Batch_DeleteData b = new f42_Batch_DeleteData();
   b.soql = s;
   Database.executeBatch(b);
  }
 }

 /* start */
 /*********************/
 global Database.QueryLocator start(Database.BatchableContext bc){
  return Database.getQueryLocator(soql);
 }

 /* execute */
 /*********************/
 global void execute(Database.BatchableContext bc, List<sObject> lstObjects){
  batchHelper.Batch_Execute(lstObjects);
 }

 /* finish */
 /*********************/
 global void finish(Database.BatchableContext bc){
  batchHelper.Batch_Finish();
 }
}


Helper (f42_Batch_DeleteDataHelper)
public without sharing class f42_Batch_DeleteDataHelper {
 private list<String> lstSearchObjects;
 public DateTime dtToday;
 /* Constructor */
 /*********************/
 public f42_Batch_DeleteDataHelper(){
  // list with objects to find and delete
  lstSearchObjects = new list<String>();
  lstSearchObjects.add('Object1__c'); // Finde und lösche Object1
  lstSearchObjects.add('Object2__c'); // Finde und lösche Object2 
  dtToday = Date.today();
 }

 /****************************
 Batch_Execute
 */
 public void Batch_Execute(List<sObject> lstObjects){
  if( !(lstObjects == null || lstObjects.isEmpty())){
   //try {
  delete lstObjects;
  //} catch (DmlException e) {
  // Process exception here
  //}
  }
 }

 /****************************
 Batch_Finish
 */
 public void Batch_Finish(){
  // %
 }

 /* BuildSearchString() */
 /********************/
 public list<String> buildSearchStrings(){
  // WHERE part of SOQL
  String wherePart = 'CreatedDate < ' + dtToday.format('yyyy-MM-dd\'T\'HH:mm:ss.000\'Z\'');
  // build search string
  list<String> lstSearch = new list<String>();
  // build soql search string
  for(String s :lstSearchObjects){
   lstSearch.add('SELECT Id FROM ' + s + ' WHERE ' + wherePart);
  }
 return lstSearch;
 }
}



Test (f42_Test_Batch_DeleteData)
@isTest
private class f42_Test_Batch_DeleteData {
 private static Account acc;
 private static list<Object1__c> lstObj1 = new list<Object1__c>();
 private static list<Object2__c> lstObj2 = new list<Object2__c>();
 private static list<f42_Order_Positions__c> lstPositions = new list<f42_Order_Positions__c>();

 /*********************
 test_BatchDeleteData
 just to arrive more test coverage for batch class*/
 static testMethod void test_BatchDeleteData() {
  createTestData();
  Test.startTest();
  f42_Batch_DeleteData newBatch = new f42_Batch_DeleteData();
  newBatch.execute(nulllstObj1);
  Test.stopTest();
 }

 /*********************
 test batch execute method */
 static testMethod void testBatchMethod_Execute() {
  createTestData();
  Test.startTest();
  f42_Batch_DeleteDataHelper batchHelper = new f42_Batch_DeleteDataHelper();
  batchHelper.dtToday = date.today().addDays(1);
  //EXECUTE
  for(String s : batchHelper.buildSearchStrings()){
   list<sObject> lstSobjects = database.query(s);
   batchHelper.Batch_Execute(lstSobjects);
  }
  //check
  list<Object1__c> lst1 = [select id from Object1__c];
  list<Object2__c> lst2 = [select id from Object2__c];
  
  system.assert(lst1.isEmpty());
  system.assert(lst2.isEmpty());
  Test.stopTest();
 }

 /*********************
 test batch finish method */
 static testMethod void testBatchMethod_Finish() {
  Test.startTest();
  f42_Batch_DeleteDataHelper batchHelper = new f42_Batch_DeleteDataHelper();
  //FINISH
  batchHelper.Batch_Finish();
  Test.stopTest();
 }

 /*********************
 test for schedule batch */
 static testMethod void testScheduleForBatch(){
  Test.startTest();

  Date dtToday = Date.today();
  DateTime dtNextRun = DateTime.newInstance(dtToday.year(), dtToday.month(), dtToday.day(), 1, 0, 0);
  dtNextRun = dtNextRun.addDays(1);

  String ss = String.valueOf(dtNextRun.second());
  String mm = String.valueOf(dtNextRun.minute());
  String hh = String.valueOf(dtNextRun.hour());
  String d = String.valueOf(dtNextRun.day());
  String m = String.valueOf(dtNextRun.month());
  String y = String.valueOf(dtNextRun.year());

  String jobName = 'TEST Batch: ' + dtNextRun.formatLong();
  String ScheduleParam = '00' + ' ' + mm + ' ' + hh + ' ' + d + ' ' + m + ' ? ' + y;

  //Start Scheduled Job
  String jobId = System.schedule(jobName, ScheduleParam, new f42_Batch_DeleteData());

  //Get the information from the CronTrigger API object
  CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :jobId];

  // Verify the expressions are the same
  System.assertEquals(ScheduleParam, ct.CronExpression);

  // Verify the job has not run
  System.assertEquals(0, ct.TimesTriggered);

  Test.stopTest();
 }

 /******************
 Test Data
 **/
 private static void createTestData(){
  Integer maxNumber = 10;
  // ACCOUNT
  acc = f42_TestDataGenerator.createTestAccount(1, true);
  system.assertnotequals(null, acc.id);
  // OBJECT 1
  for(Integer i = 1; i<=maxNumber; i++){
   lstObj1.add(f42_TestDataGenerator.createTestObject1(i, acc.Id, false));
  }
  insert lstObj1;
  system.assertEquals(maxNumber, lstObj1.size());
  // OBJECT 2
  for(Integer i = 1; i<=maxNumber; i++){
   lstObj2.add(f42_TestDataGenerator.createTestObject2(i, acc.Id, false));
  }
  insert lstObj2;
  system.assertEquals(maxNumber, lstObj2.size());
 }
}


Testdata Generator (f42_TestDataGenerator)
public without sharing class f42_TestDataGenerator {
 //Account
 public static Account createTestAccount(Double i, Boolean insertObject){
  Account a = new Account();
  a.Name = 'Test ' + i;
  a.BillingStreet = 'Musterstr.';
  a.BillingPostalCode = '81671';
  a.BillingCity = 'München';
  a.BillingCountry = 'Germany';

  if(insertObject)
   insert a;

  return a;

 }

//Object1
 public static Object1__c createTestObject1(Double i, Boolean insertObject){
  Object1__c obj = new Object1__c();
  obj.Name = 'Test ' + i;

  if(insertObject)
   insert obj;

  return obj;

 }

//Object2
 public static Object2__c createTestObject2(Double i, Boolean insertObject){
  Object2__c obj = new Object2__c();
  obj.Name = 'Test ' + i;

  if(insertObject)
   insert obj;

  return obj;

 }
}

Kommentare

Beliebte Posts aus diesem Blog

Zeitgesteuerter Flow blockiert Custom Leadkonvertierung

Die programmierte Konvertierung eines Leads bricht mit der Fehlermeldung "Unable to convert lead that is in use by workflow" ab. Der Grund ist ein Prozess, der automatisiert und zeitgesteuert ausgeführt wird. Dieser Prozess ruft zu einem späteren Zeitpunkt einen Flow auf. Während der Speicherung eines Leads wird dabei automatisch ein Flow Interview erstellt. Dieser Datensatz vom Typ "FlowInterview" blockiert die Leadkonvertierung. Lösung: Unmittelbar vor der Leadkonvertierung eine Checkbox auf dem Lead auf TRUE setzen. Da dieselbe Checkbox in den Process Builder Kriterien eingebunden ist und der Prozess nur auf den FALSE Wert reagiert, löscht das System automatisch das entsprechende Flow Interview.

Salesforce Community URL Settings

Ich habe mich in den letzten Tagen etwas ausführlicher mit Salesforce Communities in Kombination mit der API beschäftigt. Ein Problem dabei war, den richtigen Endpoint zu berechnen, wie im letzten Beitrag beschrieben API im Salesforce Partner Portal. Um die Weichen im Code für Community Benutzer einzubauen, muss während der Laufzeit berechnet werden, in welchem Context sich der aktuell eingeloggte Benutzer befindet. Dabei muss man sich zwangsweise mit den Fragen folgender Art beschäftigen: ist der eingeloggte Benuter ein Community Benutzer? ob und welche Community ist gerade aktiv? wie sieht die definierte Community URL aus? Antwort auf die Frage 1: private Boolean isCommunityUser(){         Boolean bIsCommunityUser = false;         String sUserType = UserInfo.getUserType();         sUserType = sUserType.toUpperCase();         if(sUserType == 'STANDARD')                 bIsCommunityUser = false;         if(sUserType == 'PARTNER')                  bIsCommunity

Bad value for restricted picklist field

Der Einsatz von "Restricted Picklists" bereitet spätestens im Deployment Kopfschmerzen. Basiert das Deployment auf Basis eines Drittanbietertools, dann sind die Kopfschmerzen noch intensiver. In meinem Fall habe ich versucht, ein neues Picklist-Feld mit Copado zu deployen. Während der Bereitstellung bekomme ich die folgende Fehlermeldung: System.DmlException: Insert failed. First exception on row 0; first error: INVALID_OR_NULL_FOR_RESTRICTED_PICKLIST, bad value for restricted picklist field: Z012: [CountryGroup__c] Das neue Picklist-Feld übernimmt alle Werte aus einem Global Value Set. Das bedeutet, die Option "Restrict to the values defined in the value set" ist automatisch aktiv und lässt sich nicht deaktivieren. Eine APEX-Testklasse beschreibt ebenfalls die neue Pickliste. Mit dem folgenden Workaround konnte ich das Deployment-Problem lösen: 1) Global Value Set samt Pickliste per Changeset in die Zielorg übertragen und bereitstellen ggf. Prof