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

Salesforce - Datensätze sperren (Umsetzung)

Record Lock - nun, die Umsetzung.Wir haben bereits mehrere VisualForce Pages im Einsatz. Aus dem Grund ist das zuvor beschriebene Konzept darauf zugeschnitten. Selbstverständlich lässt sich der Code auch für Standard und Custom Objects verwenden, ohne den Einsatz von VisualForce Pages. Na ja, mindestens eine VF Page brauchen wir eigentlich schon - diejenige, die von dem dem "Edit" Button aufgerufen wird.

Ich zeige jetzt die wichtigsten Bausteine aus der Umsetzung.
1) Diese VF Page wird aufgerufen sobald man den "Edit" Button betätigt

<apex:pagestandardController="myObject"title="{!myObject.Name}"extensions="LockController"action="{!SwitchToPage}">
<!-- READ MODE --> <apex:includepageName="myObjectRead"rendered="{!isEditMode == false}"/>
<!-- EDIT MODE --> <apex:includepageName="myObjectEdit"rendered="{!isEditMode == true}"/>
</apex:page>

Folgendes passiert …

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. Profilberechtigungen anpa…

Salesforce.com Certified Advanced Administrator (WI15)

Im Vergleich zu den beiden Consultants Zertifizierungen war die Advanced Admin ein Spaziergang!
Allerdings ganz ohne Vorbereitung schafft man die Prüfung auch nicht.


Hier sind einige Fragen/Antworten aus der aktuellen Prüfung.

1. Capabilities of collaborative forecasting?
- Rename categories
- Forecast using opportunity splits
- Overlay Quota
- Add categories
- Select a default forecast currency setting

2. We have four Sales Regions. Each of the region's VPs wants to have a dashboard emailed every Monday.
- Create a separate dashboard for each VP
- Create one dashboard using Visualforce
- Create one dashboard that includes a filter for each region
- Create a reporting snapshot

3. User is trying to access content in a library, but receiving an insufficent privileges message.
How to troubleshoot?
- has the user's profile the CRM content permission enabled
- user has been granted permission "Viewer" to the library
- user's record has the CRM content permisson enabled
- …