Search

Sunday, 16 August 2015

A Generic Recursive Runtime Decision Making Batch Class


Previously you could only execute 5 batches jobs from any single context
But one of my ideas on IdeasExchange was included in a recent Salesforce Release now you execute up to 100 batches which become queued in the AsyncApexJob object

What I'd like to cover in this blog, a generic recursive runtime decision making batch class.
We will make a class that requires little change and can serve and batch processing for any batch.
There are situations whereby once a batch has fully executed you want to initiate another batch:

1.      The first batch executes as many operations as it can and then initiates a decision process that either executes the same batch process again or ends the executions

            Situations where this scenario can be used:
a.       A callout to a 3rd party system and you dont know how many records exist in the 3rd party system


2.      After the first batch executes, records are set into a state that now allows a different batch to execute. Of course the second batch could be scheduled for a certain time but there is no way of knowing when the 1st batch will complete and so you have to space batches apart. If it is important to complete the operations in a timely fashion you will want to execute the 2nd batch immediately when the 1st batch completes

            Situations where this scenario can be used:
a.       The 1st batch updates a field on the Account which fires a trigger and workflows. This sets conditions on say the Contact object by updating various fields. The 2nd batch now picks up records on the Contact where this field has been updated. So we need the 1st batch to complete for the 2nd to process.


Lets consider a situation where a batch makes a call to a 3rd party system requesting for a number of records, but due to payload limitations the 3rd party can only return a certain number of records and the 3rd party doesn't provide a means of identifying how many records there are in the 3rd party because such a call drains system resources on the 3rd party.
So we need to setup a batch class that makes a call to the 3rd party and retrieves X number of records. When the batch falls into the Finish() we call a decision method which identifies how many records were processed which tells us if we have processed the last batch or not.






public with sharing class Constants {
            public static final string CONST_DOWNLOAD = 'DOWNLOAD 3rd PARTY';
}






global class batchProcess implements Database.Batchable<sObject>, Database.Stateful, Database.AllowsCallouts{
            global integer mx;                               //number of records to process
            global String batchType;                     //identifies which batch processing to call
            global String soql;                               //the soql query if the batch is to make a query to feed records into the Execute()
            global Map<String,String> vars;         //this holds any arguments that are to be passed to the batch function in the Execute()
            global Boolean success;                      //determines if the last batch execution was successful, if it wasnt we might decide to stop any further batch processing since there is a possibly a fault has been encountered

            global batchDownloadCurrentGlobals(String thisbatchType){
                        batchType = thisbatchType;
            }
           
            global batchProcessing (String thisbatchType, Map<String,String> thisvars, String thissoql){
                        batchType = thisbatchType;
                        vars = thisvars;
                        soql = thissoql;
            }
           
            global Database.QueryLocator start(Database.BatchableContext bc) {
                        if (soql == null || soql == '')
                                    return Database.getQueryLocator('Select id From User limit 1');                        else
                                    return Database.getQueryLocator(soql);
            }
           
            global void execute(Database.BatchableContext BC, List<sObject> glbs){
                                    if (batchType == Constants.CONST_DOWNLOAD){//identifies the batch type we are calling, for a different batch you simply introduce another if statement                           
                                                if (vars.containskey('Max') && vars.get('Max') != '0'){
                                                            List<ApexClass> apxCls = (List<ApexClass>)glbs;
                                                            String maxCls = vars.get('Max');
                                                            mx = integer.valueof(maxCls)-1;       

                                    //call method that retrieves "mx" number of records from the 3rd party, if the callout can be made and is successful this returns true to "success". You could introduce a for loop here to make the callout a maximum of 10 times to reduce on the number of batch operations
                                    success = Utils.retrieveData(mx);
                                    }
                        }
            }

            global void finish(Database.BatchableContext BC){
                        if (batchType == Constants.CONST_DOWNLOAD ){
                                    if (success)//identifies the last callout was successul
                                                Utils.decideToRunAgain(mx);
                                    else
                                                //do something when last batch didnt process and encountered an issue
                        }
            }





This is the decision method:


            public static void decideToRunAgain (integer mx){
                        //This custom setting is set in retrieveData() for the number of records retrieved from the 3rd party in the last callout made in the batch Execute() if this number is less than "mx" the last callout was the last callout required
                        Configurations__c latestCall = Configurations__c.getinstance(Constants.CONST_MAX);
        integer newlatestCallInt = (latestCall != null) ? integer.valueof(latestCall.Value__c) : 0;

                        if (newlatestCallInt == mx){
                                    //the last callout retrieved the same number of records as was requested so this cannot be the last callout to make so a new batch can be created
                                   
                                    //we also need to check that the number of queued batches is less than 100 otherwise the maximum in the queue has been reached
                                    //unfortunately we cannot halt execution for a time or even continually check AsyncApexJob in a for loop waiting for the queue to drop because that would hit governor limits
//Note:JobType ='Batch Apex' identifies a batch being processed, the JobType ='Batch Apex Worker' identifies the latest record being processed in the batch and so is constantly changing
                                    if ([Select id From AsyncApexJob where JobType ='Batch Apex' and Status = 'Holding'].size() < 100){
                                                batchProcessing batch = new batchProcessing (Constants.CONST_DOWNLOAD, <<specify the other parameters>>);
                    Database.executeBatch( batch, 1 );
                                    }
        }             
            }




           

The are various different themes you can employ on this concept, such as all the logic could be pulled completely outside of the batch into separate classes, keeping the batch class lightweight and actually never needs to change

Further information
http://releasenotes.docs.salesforce.com/en-us/spring15/release-notes/rn_apex_flex_queue_ga.htm?edition=&impact=



Saturday, 18 July 2015

Object Definition Capture

I launched my 2nd app last week, which is a free app; so Id like to take the opportunity in this newsletter to do a bit of self promotion. Hopefully my readers will like this free app.

I don't want to send you to sleep but I'd like to tell you a short story.

In every company Ive worked in so far Product Owners, Business Analysts etc often have different styles of capturing requirements and recording them. Unfortunately often is the case that  requirements are not captured accurately or completely. The net affect is that the developer or administrator does not produce what is expected and the tester is not sure of what to test. The developers and administrators have to have additional multiple meetings until the full requirements are captured completely and recorded, which results in a lot of wasted time.
This frequently occurs when capturing requirements for objects and fields, but this should be the easiest of all types of requirements to capture because the number of different permutations that could be captured in the requirements is actually finite, unlike that of a code related project.

So I decided to create an app that would solve 4 main issues when capturing requirements for Objects and Fields.


                                    Capture object and field requirements in a consistent way for all Product Owners, Business Analysts
                                    
                                   Save time for Product Owners and Business Analysts when capturing requirements
                                    
                                   Developers and testers are given a consistent approach and so requirements are easily understood

                                  Product Owners and Business Analysts accurately capture all relevant requirements


15 companies have already installed my app in just 4 days since it was launched.


You need to add your org to Remote Site Settings, so if your org is say https://na12.salesforce.com, you would add this URL to Remote Site Settings. The bold part is the part which will be different for your Salesforce org.

Another benefit that the app could provide for you is that it will find all the Report Types and Page Layouts that you have in your system and enter them into 2 Custom Objects. You may be able to use this data for other purposes.

I hope you find the app beneficial.


Structuring Salesforce Invocable Methods

Invocable Methods are an important addition to the platform and Ive been calling for a complete overhaul of the Salesforce workflow engine for a long time and finally Process Builder was introduced a couple of releases back. 

There a few issues and limitations of Invocable Methods however:

  1. Passing Sobject lists appears not to work and instead you have to pass Ids of the Sobjects and then perform a SOQL to get the Sobjects, so you have to be mindful of SOQL limits. I would expect Salesforce to fix this issue in future releases, so this might go away.


  1. Another limitation is that you can only have 1 Invocable Method per class, which means you can end up having many classes. There is a neat way to get around this however:

Lets consider 2 examples. One Invocable Method sends an email introducers customers to The Self Evolving Software app, the 2nd Invocable Method updates a field on the Account to make post codes correctly formatted. 


@InvocableMethod(label='Setup_Salesforce_To_Salesforce_Email' description='Sends a Email To Explain Setup Of Salesforce To Salesforce')
public static void sendBenefitsEmail(ID[] emls){
      Attachment[] att = [Select id,Body, Name From Attachment
      where name='SES Introduction.pdf'];
       
      UtilEmail.sendEmail(emls,'How To Use The Self Evolving Software App', att);        
}




 @InvocableMethod(label='Format postcode on Account' description='Format postcode on Account')
public static void formatPostcode(ID[] accs){
      Account[] acc = [Select BillingPostalCode From Account
      where Id In :accs];
      for (Account eachacc : acc){
            eachacc.BillingPostalCode =
            UtilFormats.postcodeFormatting(eachacc.BillingPostalCode);
      }

      update acc;
}








The issue here is that we would have to create 2 classes with 1 Invocable Method in each.

The solution is to combine all Invocable Methods into an InvocableMethodHandler class and call to separate classes where the actions will take place




We first need to create a wrapper class to hold our data

In the labels for each InvocableVariable you probably want to display something a bit shorter as this will appear in the process builder.

 public with sharing class KeyValueInv {
      @InvocableVariable(label='Optional key if you want to represent a collection' required= false)
      public String key;
      @InvocableVariable(label='Stores the value' required= true)
      public String value;
      @InvocableVariable(label='The data type of the value' required=true)
      public String fieldtype;
      @InvocableVariable(label='This identifies the function to call' required=true)
      public String type;
     
      public KeyValueInv(String thiskey, String thisvalue, String thisfieldtype, String thistype){
            this.key = thiskey;
            this.value = thisvalue;
            this.fieldtype = thisfieldtype;
            this.type = thistype.toUpperCase();
      }


}

Below is the only invocable class and method you will ever need to create as we can send to it any type of data to the KeyValueInv class, plus in the InvocableMethodHandler we identify the function to call

public with sharing class InvocableMethodHandler {
      @InvocableMethod(label='' description='')
      public static void processType(KeyValueInv[] kvLst){
            if (kvLst[0].type == 'Send Benefits Email'){
                  UtilClass.sendBenefitsEmail(kvLst);
            }
            else if (kvLst[0].type == 'Format Postcode'){
                  UtilClass.formatPostcode(kvLst);
            }
           
      }

}



 public static void sendBenefitsEmail(kvLst[] kvs){
    //code .....
 }


public static void formatPostcode(kvLst[] kvs){
    //code .....
}





Now when you create your process builder you will be able to set the 'Send Benefits Email' as or 'Format Postcode' and this will decide which function to ultimately to call.

Saturday, 13 June 2015

Intro to Unit Test Data Creation Framework continued…….



In my last blog http://stevefouracre.blogspot.co.uk/2015/06/intro-to-unit-test-data-creation.html I gave examples of how the framework can be used. I also introduced rapidProcessing. Now we will expand the framework making use of rapidProcessing.

Reminder:
            rapidProcessing allows you to bypass the code in triggers, allowing the test            data to be created much faster ( this can also be useful when you are migrating          data into Salesforce ). In both of these situations you are telling Salesforce         exactly what data to create and you don't want the system to manipulate the          data further or to perform any actions within the triggers that may do all kind    of things such as creating additional business process data like Tasks, Events           and Cases etc, or sending out emails to customers etc ( however the latter wont             happen in unit tests as emails are not sent from unit tests ).

Lets take our previous example which will bypass both the Account and Contact triggers:


KeyValue[] kvsA = new KeyValue[]{};
KeyValue[] kvsC = new KeyValue[]{};

Map<System.Type, KeyValueBulk> keyMap = new Map<System.Type, KeyValueBulk>();
keyMap.put(Account.class, new KeyValueBulk(1, kvsA));
keyMap.put(Contact.class, new KeyValueBulk(5, kvsC));

TriggerController.rapidProcessing = new Map<System.Type, Boolean>{ Account.class => true, Contact.class => true};

//now rapidProcessing has been turned on for both objects the code in the triggers will be bypassed. You will need to build into your triggers the Trigger Control Framework:

TestDataComplexData dataCl = new TestDataComplexData ();
dataCl.insertAccountAndContacts(keyMap);



Ok for the above example for triggers to be bypassed we need to first create 2 Hierarchical custom settings:

Triggers_Off__c
            This custom settings needs to have the following fields:
           
Field Name
Data Type
value
Boolean


Trigger_Per_Object__c
            This custom settings needs to have the following fields:

Field Name
Data Type
Account
Boolean
Contact
Boolean

            For each additional trigger you create you will need to create an additional             field in this custom setting for that trigger and you will need to add a new else if {} statement to the globalTriggerControlSetting() function in the       TriggerController class. An example of this will be shown next for the        Account and Contact

The 2 custom settings above can be used to bypass the triggers typically when either you are making a deployment to Production or if you are performing a data migration. They will allow you to disable individual or all triggers per person, per Profile or the entire system.


In the TriggerController class we created a number of variables to bypass the Account trigger, now create a similar set of variables for the Contact trigger. We also need to add 2 functions into the class globalTriggerControlSetting() and globalTriggerPerObjectControlSetting():



            //Contact - Only for testing to check if the code ran or not
            public static boolean Contact_Update_Succeeded = false;
            public static boolean Contact_Insert_Succeeded = false;
            public static boolean Contact_Delete_Succeeded = false;
            public static boolean Contact_UnDelete_Succeeded = false;

            //Contact - Disable / Enable parts of trigger
            public static boolean Contact_DisableAllTypes = false;
            public static boolean Contact_DisableInsert = false;
            public static boolean Contact_DisableUpdate = false;
            public static boolean Contact_DisableDelete = false;
            public static boolean Contact_DisableUnDelete = false;

public static boolean globalTriggerControlSetting(){
            return (((Triggers_Off__c.getOrgDefaults() != null) ? Triggers_Off__c.getOrgDefaults().value__c : false) || Triggers_Off__c.getInstance(UserInfo.getUserId()).value__c  || Triggers_Off__c.getInstance(UserInfo.getProfileId()).value__c) ;
}

public static boolean globalTriggerPerObjectControlSetting(String obj){
Trigger_Per_Object__c.getInstance(UserInfo.getProfileId()));
            if (obj == 'Account__c') return (((Trigger_Per_Object__c.getOrgDefaults() != null) ? (boolean)Trigger_Per_Object__c.getOrgDefaults().Account__c  : false) || (boolean)Trigger_Per_Object__c.getInstance(UserInfo.getUserId()).Account__c || (boolean)Trigger_Per_Object__c.getInstance(UserInfo.getProfileId()).Account__c) ;
            else if (obj == 'Contact__c') return (((Trigger_Per_Object__c.getOrgDefaults() != null) ? (boolean)Trigger_Per_Object__c.getOrgDefaults().Contact__c  : false) || (boolean)Trigger_Per_Object__c.getInstance(UserInfo.getUserId()).Contact__c || (boolean)Trigger_Per_Object__c.getInstance(UserInfo.getProfileId()).Contact__c) ;
            else return false;
}



The Disable variables eg: Contact_DisableAllTypes allows you to disable all of a trigger or individual parts of a trigger, commonly will be used within the body of the code including unit tests. We could have used the custom settings but that would involve using DMLs to turn the triggers on and off.



In the next blog we will create the code for the trigger.

Friday, 5 June 2015

Intro to Unit Test Data Creation Framework continued…….





In my last blog I laid out the codebase required for the framework.

Now you have created the structure of the framework, lets run some examples.
But before we do I'd like to point out 1 thing, in the framework you have various options depending how you personally feel you would like to structure the framework. You can make the TestDataComplexData class extend the TestDataBulkData class instead of the TestDataInsertData and you may want to rename the TestDataComplexData class to something like TestDataCreation class and so it is more generic. These are just options for you to play around with.


To perform no DML and just return a new Contact using the standard json string you can add a returnContact function in the TestDataReturnData class:


The new function will look like:

    public Contact returnContact(KeyValue[] kVals){
            
        return (Contact) (super.returnAnyObject(new TestDataFramework_JsonLibrary.Standard().M.get('CONTACT'), kVals, Contact.class)[0]);
    }



An example of using this new function:


TestDataComplexData dataCl = new TestDataComplexData ();
dataCl.returnContact(null);



To simply create a new Contact using the standard json string:


TestDataComplexData dataCl = new TestDataComplexData ();
dataCl.insertContact(null, null);



To add some values into certain fields:


KeyValue[] kvs = new KeyValue[]{};
//this will overwrite the Email field with a new email
kvs.add(new KeyValue('Email', 'myemail@yahoo.com', 'String'));

TestDataComplexData dataCl = new TestDataComplexData ();
dataCl.insertContact(null, kvs);



To simply create a new Account and Contact using the standard json string and to add a value into a field:

Map<System.Type, List<KeyValue>> keyMap = new Map<System.Type, List<KeyValue>>();
KeyValue[] kvs = new KeyValue[]{};
//this will overwrite the Email field with a new email
kvs.add(new KeyValue('Email', 'myemail@yahoo.com', 'String'));

kMaps.put(Contact.class, kvs);

TestDataComplexData dataCl = new TestDataComplexData ();
dataCl.insertContactAndAccount(kMaps);



To create 1 Account and 5 Contacts linked using the standard json string#


KeyValue[] kvsA = new KeyValue[]{};
KeyValue[] kvsC = new KeyValue[]{};

Map<System.Type, KeyValueBulk> keyMap = new Map<System.Type, KeyValueBulk>();
keyMap.put(Account.class, new KeyValueBulk(1, kvsA));
keyMap.put(Contact.class, new KeyValueBulk(5, kvsC));

TestDataBulkData dataCl = new TestDataBulkData ();
dataCl.insertAccountAndContacts(keyMap);



If your framework is setup so that TestDataComplexData class extends the TestDataBulkData class instead just change the code above to


KeyValue[] kvsA = new KeyValue[]{};
KeyValue[] kvsC = new KeyValue[]{};

Map<System.Type, KeyValueBulk> keyMap = new Map<System.Type, KeyValueBulk>();
keyMap.put(Account.class, new KeyValueBulk(1, kvsA));
keyMap.put(Contact.class, new KeyValueBulk(5, kvsC));

TestDataComplexData dataCl = new TestDataComplexData ();
dataCl.insertAccountAndContacts(keyMap);



If you now want to bypass the triggers to speed up processing time because you want to upload a lot of records to be used in your testmethod:


KeyValue[] kvsA = new KeyValue[]{};
KeyValue[] kvsC = new KeyValue[]{};

Map<System.Type, KeyValueBulk> keyMap = new Map<System.Type, KeyValueBulk>();
keyMap.put(Account.class, new KeyValueBulk(1, kvsA));
keyMap.put(Contact.class, new KeyValueBulk(5, kvsC));

TriggerController.rapidProcessing = new Map<System.Type, Boolean>{ Account.class => true, Contact.class => true};

//now rapidProcessing has been turned on for both objects the code in the triggers will be bypassed. You will need to build into your triggers the Trigger Control Framework which will be the next thing I will cover in my next post:

TestDataComplexData dataCl = new TestDataComplexData ();
dataCl.insertAccountAndContacts(keyMap);



If you want to insert an Account using the standard json string and then update that Account:


TestDataUpdateData dataCl = new TestDataUpdateData();
KeyValue[] kvsUpdate = new KeyValue[]{};
//this will overwrite the BillingPostalCode field with a new post code
kvsUpdate.add(new KeyValue('BillingPostalCode', 'EC1 2CV', 'String'));

//first argument is null because this overrides the fields for the insert part
dataCl.updateAccount(null, kvsUpdate);


If you want to insert an Account using the standard json string and then update that Account:


TestDataUpdateData dataCl = new TestDataUpdateData();

KeyValue[] kvsInsert = new KeyValue[]{};
//this will overwrite the BillingPostalCode field with a new post code
kvsInsert.add(new KeyValue('BillingPostalCode', 'SE1 2SG', 'String'));

KeyValue[] kvsUpdate = new KeyValue[]{};
//this will overwrite the BillingPostalCode field with a new post code
kvsUpdate.add(new KeyValue('BillingPostalCode', 'EC1 2CV', 'String'));

dataCl.updateAccount(kvsInsert, kvsUpdate);



If you want to create a new insert function and add this to the framework, here are the steps you will need to follow:


  1. Create a new constant in the Constant class
  2. Add a new json string to the libraryMap variable in the json library class
  3. Create a new insert function in the TestDataInsertData class similar to the insertContact() function just replacing with the new Sobject type
  4. You can now use this function in the other classes TestDataComplexData, TestDataBulkData and TestDataUpdateData to build more complex data structures if you require



Saturday, 30 May 2015

Intro to Unit Test Data Creation Framework continued.......


Read the first blog at http://stevefouracre.blogspot.co.uk/2015/05/unit-test-data-creation-framework.html

In my last blog I introduced my Unit Test Data Creation Framework, now we will start building the classes of the framework.


First create the Constants class


public class Constants {

public static final String CONST_Account = 'ACCOUNT';
public static final String CONST_Contact = 'CONTACT';

}




Now, in the ITestData class add



public interface ITestData {
    List<sObject> returnAnyObject(String jsonStr, KeyValue[] kVals);                                                 
}




Next, in the TestDataJsonLibrary class add



public class TestDataJsonLibrary {

public static String referenceKey = 'ReferenceID';
public class Standard{

public final Map<String, String> libraryMap = new Map<String, String>{
Constant.CONST_Account                            => '{"attributes":{"type":"Account"},"Field1__c":"Value 1","Field2__c":"Value 2"}',
Constant.CONST_Contact     => '[{"attributes":{"type":"Contact"},"'+referenceKey+'":"Reference Value","Field1__c":"Value 1"}
};

}

}





In the Return Data class add



public abstract class TestDataReturnData implements ITestData{

public Boolean bulkModeOn = false;

public Map<System.Type, String> overrideJson = new Map<System.Type, String>();

    public List<sObject> returnAnyObject(String jsonStr, KeyValue[] kVals){

        List<sObject> sobj;
       
        if(jsonStr.contains(TestDataJsonLibrary.referenceKey))
                                    jsonStr = getFilteredJsonString(jsonStr, kVals);
       
        if(jsonStr.startsWith('['))
            sobj = (List<sObject>) System.Json.deserialize(jsonStr, List<sObject>.class);
        else
            sobj = (List<sObject>) System.Json.deserialize('['+jsonStr+']', List<sObject>.class);
       
        if(kVals != null){
                    for(sObject obj : sobj)
                       obj = UtilDML.setObjData(obj, kVals);
        }
       
        return sobj;
    }


    private String deserialJson(String jsonStr, KeyValue[] kVals){   
            List<Object> deserialLst = (List<Object>) JSON.deserializeUntyped(jsonStr.unescapeEcmaScript());
                       
            String aReferenceKey;
       
            //when setting the fields from the KeyValues if 1 is the lookup field set to  aReferenceKey          
            for(KeyValue kv : kVals){                
                        if(kv.key == TestDataJsonLibrary.referenceKey){                           
                                    aReferenceKey = kv.value;
                                    break;
                        }
            }
           
            List<sObject> serialLst = new List<sObject>();
           
            for(Object obj : deserialLst){
                        Map<String, Object> objMap = (Map<String, Object>) obj;

                        if(aReferenceKey == objMap.get(TestDataJsonLibrary.referenceKey)){                                       
                                    objMap.remove(TestDataJsonLibrary.referenceKey);
                                    serialLst.add(UtilDML.convertToSobject(objMap));
                        }
            }
           
            return JSON.serialize(serialLst);
    }

}





In the Insert Data class add



public virtual class TestDataInsertData extends TestDataReturnData{

    private sObject insertAnyObject(String jsonStr, KeyValue[] kVals, System.Type objType){
        if(overrideJson != null && overrideJson.containsKey(objType))
            jsonStr = overrideJson.get(objType);
       
        sObject sobj = super.returnAnyObject(jsonStr, kVals)[0];
       
        // set to true if inserting multiple records
        if(bulkModeOn == false)
            insert sobj;
              
        return sobj;
    }

    public Contact insertContact(String jsonstr, KeyValue[] kVals){       
        return (Contact) insertAnyObject((jsonstr != null && jsonstr != '') ? jsonstr : new TestDataFramework_JsonLibrary.Standard().M.get(Constants.CONST_Contact), kVals, Contact.class);
    }

}




In the Update Data class add



public virtual class TestDataUpdateData extends TestDataInsertData{

    public Account updateAccount(KeyValue[] insertkVals, KeyValue[] updatekVals){      
        Account acc = super.insertAccount(insertkVals);
       
        if(updatekVals != null)
            acc = (Account) UtilDML.setObjData(acc, updatekVals);
       
        update acc;
       
        return acc;
    }

}





In the ComplexData class add



public virtual class TestDataComplexData extends TestDataInsertData{
    public Account acc{get;set;}
    public Contact cont{get;set;}
    public Opportunity opp{get;set;}


    public Account insertContactAndAccount(Map<System.Type, List<KeyValue>> keyMap){
        //inserts just 1 Account and 1 Contact and links them together, using the map in the argument means you only need to use 1 argument

        //stops a null exception occurring later in the code
        if(keyMap == null){
            keyMap = new Map<System.Type, List<KeyValue>>();
            kMaps.put(Contact.class, new List<KeyValue>());
        }else if(keyMap.containsKey(Contact.class) == false)
            keyMap.put(Contact.class, new List<KeyValue>());
       
       
        this.acc = super.insertAccount(keyMap.get(Account.class));

        // now provide the Id into the KeyValues to link the Objects together
        keyMap.get(Contact.class).add(new KeyValue('AccountId', this.acc.id, 'ID'));

       
        this.cont = super.insertContact(keyMap.get(Contact.class));
       
        return this.acc;
    }


    public Account insertContactOpportunityAndAccount(Map<System.Type, List<KeyValue>> keyMap){

            insertContactAndAccount(keyMap);

            if(keyMap.containsKey(Opportunity.class) == false)
                        keyMap.put(Opportunity.class, new List<KeyValue>());

        keyMap.get(Opportunity.class).add(new KeyValue('AccountId', this.acc.id, 'ID'));
       
        this.opp = super.insertOpportunity(keyMap.get(Opportunity.class));
       
        return this.acc;
    }

}




In the BulkData class add



public virtual class TestDataBulkData extends TestDataInsertData{

    public Account insertAccountAndContacts(Map<System.Type, KeyValueBulk> keyMap){
        //This inserts 1 Account and a number of Contacts

        //stops a null exception occurring later in the code
        if(keyMap == null){
            keyMap = new Map<System.Type, KeyValueBulk>();
            keyMap.put(Contact.class, new KeyValueBulk());
        }else if(kMaps.containsKey(Contact.class) == false)
            kMaps.put(Contact.class, new KeyValueBulk());
       
        this.conts = new List<Case>();
        Account acc = super.insertAccount(keyMap.get(Account.class) .keyValueBulkLst);
       
        bulkModeOn = true; // stops records being inserted
       
                    //link records together
                    (keyMap.get(Contact.class)).keyValueBulkLst.add(new KeyValue('AccountId', acc.id, 'ID'));
                    List<KeyValueBulk> kVals = (keyMap.get(Contact.class)).keyValueBulkLst;
                   
                    for(Integer i = 0; i < (keyMap.get(Contact.class)).insertRecs; i++)
                        this.conts.add(super.insertContact(kVals));
                   
                    insert this.conts;
       
        bulkModeOn = false; //resets flag
       
        return this.acc;
    }
}




KeyValue class looks like this



public class KeyValue{

            public String key{get; set;}
            public String value{get; set;}
            public String fieldType{get; set;}

            public KeyValue(String key, String value, String fieldType){
                       
                        this.key = key;
                        this.value = value;
                        this.fieldType = fieldType.toUpperCase();
            }

            public KeyValue(String key, String value){
                        this.key = key;
                        this.value = value;
            }

            public KeyValue(){
            }
}


KeyValueBulk class looks like this



public class KeyValueBulk{

public integer insertRecs;
public KeyValue[] keyValueBulkLst;

public KeyValueBulk(integer insRecs, KeyValue[] kys){
            insertRecs  = (insertRecs != null && insertRecs > 0) ? insertRecs : 1;
            keyValueBulkLst = kys;
}

}



Also if you have any triggers, for rapid transactional processing create the TriggerController class

To bypass code in triggers which often fire workflows and process builders, which in turn fire triggers again; all of which takes extra processing time. But when you are simply creating test data you are implicitly telling the system to create an exact data set and if you are not actually testing the trigger there is no need when you are creating the test data to run through the code in the trigger.

In the TriggerController class add




public class TriggerController {

            //used specifically in unit test data framework
            public static Map<System.Type, Boolean> rapidProcessing;

            //Account - Disable / Enable parts of trigger
            public static boolean Account_DisableAllTypes = false;
            public static boolean Account_DisableInsert = false;
            public static boolean Account_DisableUpdate = false;
            public static boolean Account_DisableDelete = false;
            public static boolean Account_DisableUnDelete = false;

            //used to unit test the trigger control to ensure the correct parts of the trigger were triggered
            public static boolean Account_Insert_Succeeded = false;
            public static boolean Account_Update_Succeeded = false;
            public static boolean Account_Delete_Succeeded = false;
            public static boolean Account_UnDelete_Succeeded = false;

}



In the UtilDML class add



public class UtilDML {

            public static Sobject setObjData(Sobject aobj, KeyValue[] kVals){
                        Sobject thisobj;
        if (kVals != null){
            for (KeyValue eachval : kVals){
                        try{
                                    thisobj = setFieldVal(aobj, eachval);
                        }
                        catch(Exception ex){system.debug('## ex ' + ex); }
            }
        }
       
        return thisobj;
            }

            public static Sobject setFieldVal(Sobject obj, KeyValue thisKeyVal){
                            
                        system.debug('## thisKeyVal ' + thisKeyVal);
                        if (thisKeyVal.fieldtype == 'DATE'){
                                    String tmpDt = thisKeyVal.value;
                                    Date dt = Date.valueOf(tmpDt.substring(0,9).trim());
                                    system.debug('## dt ' + dt);
                                    obj.put(thisKeyVal.key, dt);
                        }else if (thisKeyVal.fieldtype == 'DATETIME'){
                                    system.debug('## Datetime.valueOf(tmpDt) ' + Datetime.valueOf(thisKeyVal.value));
                                    obj.put(thisKeyVal.key, Datetime.valueOf(thisKeyVal.value));
                                    system.debug('## obj ' + obj);
                        }else if (thisKeyVal.fieldtype == 'DECIMAL')
                                    obj.put(thisKeyVal.key, decimal.valueof(thisKeyVal.value));
                        else if (thisKeyVal.fieldtype == 'INTEGER')
                                    obj.put(thisKeyVal.key, integer.valueof(thisKeyVal.value));
                        else if (thisKeyVal.fieldtype == 'LONG')
                                    obj.put(thisKeyVal.key, long.valueof(thisKeyVal.value));
                        else if (thisKeyVal.fieldtype == 'DOUBLE')
                                    obj.put(thisKeyVal.key, Double.valueof(thisKeyVal.value));
                        else if (thisKeyVal.fieldtype == 'BOOLEAN')
                                    obj.put(thisKeyVal.key, ((thisKeyVal.value).toUpperCase() == 'TRUE') );
                        else if (thisKeyVal.fieldtype == 'BLOB')
                                    obj.put(thisKeyVal.key, Blob.valueof(thisKeyVal.value));
                        else if (thisKeyVal.fieldtype == 'ID'){
                                    obj.put(thisKeyVal.key, ((ID)thisKeyVal.value));
                        }else
                                    obj.put(thisKeyVal.key, thisKeyVal.value);//String
                       
                        system.debug('## obj ' + obj);
                        return obj;
                       
            }

 public static sObject convertToSobject(Map<String, Object> objMap){

     sObject sObj = Schema.getGlobalDescribe().get((String)((Map<String, Object>)objMap.get('attributes')).get('type')).newSObject();
     Map<String, Schema.SObjectField> sObjMap = sObj.getSObjectType().getDescribe().fields.getMap();

     for(String key : objMap.keySet()){
    
     if(sObjMap.containskey(key)){
    
     Schema.DescribeFieldResult field = sObjMap.get(key).getDescribe();
    
     String fieldType = field.getType().Name();
     String value = (String) objMap.get(key);
    
if(fieldType == 'DATE'){ 
sObj.put(key, Date.valueOf(value));
}else if(fieldType == 'DATETIME'){
sObj.put(key, Datetime.valueOf(value));
}else if(fieldType == 'DECIMAL'){
//sObj.put(key, Decimal.valueof(value));
sObj.put(key, Decimal.valueOf(value));
}else if(fieldType == 'INTEGER'){
sObj.put(key, Integer.valueOf(value));
}else if(fieldType == 'LONG'){
sObj.put(key, Long.valueOf(value));
}else if(fieldType == 'DOUBLE'){
sObj.put(key, Double.valueOf(value));
}else if(fieldType == 'BOOLEAN'){
sObj.put(key, (value.toUpperCase() == 'TRUE'));
}else{// String
sObj.put(key, value);
}
     }
     }
    
     return sObj;
    }

}



 In my next blog we will start building some examples of using the framework. If you have any questions about the framework please add comments on my blog