Building Reusable Process that Generate Test Data

When we create a new org in many cases it is empty and we need some test data.

In a number of previous instances, I required a large number of Leads entries, therefore I wrote a script to produce 100 Leads using random data. Later on, though, I found that I wanted something similar but for a different data type, so I decided to turn my script into a reusable method. 

In this article, I'll go over the conversion process step-by-step, along with the problems it encountered and how it was resolved, as well as a few potential shortcomings.


On parts of code that we occasionally run, such data fix, a similar process can be carried out. However, the benefit versus cost factors must be taken into account. How different each run from the previous one is, how frequently we use it, and of course, how much it cost to develop.


Here is my initial script, which generated 100 records for leads. Everything is plainly hardcoded, therefore this script is solely useful for producing Leads, as you can see.

List<Lead> newLeads = new List<Lead>();

List<String> ratingList = new List<String>{'Hot', 'Warm', 'Cold'};
Integer totalRatings = ratingList.size();

List<String> indList = new List<String>{'Agriculture', 'Apparel', 'Banking', 'Education', 'Consulting'};
Integer totalnd = ratingList.size();

List<String> countryList = new List<String>{'Israel', 'Spain', 'USA', 'Italy', 'Sweden', 'Brazil', 'Chile'};
Integer totaCountry = ratingList.size();

for(Integer index = 0; index < 100; index ++){
	newLeads.add(new Lead(
		FirstName = 'Test',
		LastName = 'Lead ' + index,
		Company = 'DemoLead ' + index,
		Email = 'test.lead' + index + '@test.com',
		Rating = ratingList.get(Integer.valueof((Math.random() * totalRatings))),
		Industry = indList.get(Integer.valueof((Math.random() * totalnd))),
		Status = 'Working - Contacted',
		LeadSource = 'Web',
		Country = countryList.get(Integer.valueof((Math.random() * totaCountry)))));
}

insert newLeads;


From Script to Method

I will first create an Apex Class with a method and add the script to it. Obviously, the process isn't truly random at the present, but we'll take care of that.

public with sharing class DataCreator{
	public static void generateRandomData(){
	
		List<Lead> newLeads = new List<Lead>();

		List<String> ratingList = new List<String>{'Hot', 'Warm', 'Cold'};
		Integer totalRatings = ratingList.size();

		List<String> indList = new List<String>{'Agriculture', 'Apparel', 'Banking', 'Education', 'Consulting'};
		Integer totalnd = ratingList.size();

		List<String> countryList = new List<String>{'Israel', 'Spain', 'USA', 'Italy', 'Sweden', 'Brazil', 'Chile'};
		Integer totaCountry = ratingList.size();

		for(Integer index = 0; index < 100; index ++){
			newLeads.add(new Lead(
				FirstName = 'Test',
				LastName = 'Lead ' + index,
				Company = 'DemoLead ' + index,
				Email = 'test.lead' + index + '@test.com',
				Rating = ratingList.get(Integer.valueof((Math.random() * totalRatings))),
				Industry = indList.get(Integer.valueof((Math.random() * totalnd))),
				Status = 'Working - Contacted',
				LeadSource = 'Web',
				Country = countryList.get(Integer.valueof((Math.random() * totaCountry)))));
		}

		insert newLeads;
	}
}


Since the code should be able to construct any data type, we cannot use the object name Lead in the method. Instead, we will change List<Lead> to ListSObject>.


public with sharing class DataCreator{
	public static void generateRandomData(){
	
		List<sObject> newRecords = new List<sObject>();

		...

		insert newRecords;
	}
}



Each object has a unique set of fields, it should not refer to them by name. Therefore, we will add the object type as parameter and use the describe method to access its fields.

A list of the mandatory fields for the object type should also be kept because each record must have data for them.


public with sharing class DataCreator{
	public static void generateRandomData(String objectType){
	
		List<sObject> newRecords = new List<sObject>();

		//Get all object fields
		SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
		Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
		
		//Store list of require fields
		List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
		
		for(Schema.SObjectField sField : fieldMap.values()){
			Schema.DescribeFieldResult dfr = sField.getDescribe();
			
			if(! dfr.isNillable() && dfr.isCreateable()){
				requireFields.add(dfr);
			}
		}
		
		

		insert newRecords;
	}
}


Let's build some records next. For this, I included a parameter that specifies how many records it should generate, as well as a loop that creates the new records with the require fields.


public with sharing class DataCreator{
	public static void generateRandomData(
		String objectType, 
		Integer totalRecords){
	
		List<sObject> newRecords = new List<sObject>();

		//Get all object fields
		SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
		Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
		
		//Store list of require fields
		List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
		
		for(Schema.SObjectField sField : fieldMap.values()){
			Schema.DescribeFieldResult dfr = sField.getDescribe();
			
			if(! dfr.isNillable() && dfr.isCreateable()){
				requireFields.add(dfr);
			}
		}
		
		for(Integer index = 0; index < totalRecords; index ++){
			SObject newRecord = objectRef.newSobject();
		
			for(Schema.DescribeFieldResult dfr : requireFields){
				if(dfr.getType() == Schema.DisplayType.Picklist || dfr.getType() == Schema.DisplayType.MultiPicklist){                      
					newRecord.put(dfr.getName(), dfr.getPicklistValues().get(Integer.valueof((Math.random() * dfr.getPicklistValues().size()))).getValue());
				}
				else if(dfr.getType() == Schema.DisplayType.String || dfr.getType() == Schema.DisplayType.TextArea || dfr.getType() == Schema.DisplayType.URL){
					newRecord.put(dfr.getName(), 'test1');
				}
				else if(dfr.getType() == Schema.DisplayType.Integer || dfr.getType() == Schema.DisplayType.Long || dfr.getType() == Schema.DisplayType.Double){
					newRecord.put(dfr.getName(), 1);
				}
				else if(dfr.getType() == Schema.DisplayType.Date || dfr.getType() == Schema.DisplayType.DateTime){
					newRecord.put(dfr.getName(), System.today());
				}
				else if(dfr.getType() == Schema.DisplayType.Boolean){
					newRecord.put(dfr.getName(), false);
				}
				else if(dfr.getType() == Schema.DisplayType.REFERENCE) {
				
					set<String> sTypes = new set<String>();
					
					for(Schema.sObjectType refTo : dfr.getReferenceTo()){
						sTypes.add(refTo.getDescribe().getName());
					}
					
					if(sTypes.contains('User')){
						newRecord.put(dfr.getName(), UserInfo.getUserId());
					}
					else if(sTypes.contains('Profile')){
						//Special logic for profile as we cannot create profiles will use the profile of the running user
						newRecord.put(dfr.getName(), UserInfo.getProfileId());
					}
				}
				else{
					//Unsupported type is require - need to improve the code
					System.debug('Require field is not populated: ' + dfr.getName() + ' ' + dfr.getType());
				}
			}
			
			newRecords.add(newRecord);
		}
		
		

		insert newRecords;
	}
}


You can see that the code check for the type of each field and sets a dummy value accordingly.  Should also notice that not all field types are supported.


It may already be used with the following commands to generate some test data:

DataCreator.generateRandomData('Lead', 5);

DataCreator.generateRandomData('Account', 50);


While it might work for a few cases, it is insufficient for most cases.

Typical use cases that are missing:

  • Fill in any more fields that are not required.
  • Set particular value(s) for particular fields


I used an inner class type that will hold settings on how to fill up particular fields in order to get greater flexibility.

For each field that we want to populate, we will send the field name as well as list of optional values. The method will randomly set one of the value for each record.


public class FieldSettings{
	private String fieldName;
	private List<object> optionalValues;
	
	public FieldSettings(String fieldName, List<object> optionalValues){
		this.fieldName = fieldName;, 
		this.optionalValues = optionalValues;
	}
}


We can now improve the function to accept a list of FieldSettings as an parameter and utilize it when creating records.

Also notice that the code now set a require field only if it is empty. Meaning, it was not populated by the field settings.


public with sharing class DataCreator{
	public static void generateRandomData(
		String objectType, 
		Integer totalRecords,
		List<FieldSettings> fieldsList){
	
		List<sObject> newRecords = new List<sObject>();

		//Get all object fields
		SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
		Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
		
		//Store list of require fields
		List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
		
		for(Schema.SObjectField sField : fieldMap.values()){
			Schema.DescribeFieldResult dfr = sField.getDescribe();
			
			if(! dfr.isNillable() && dfr.isCreateable()){
				requireFields.add(dfr);
			}
		}
		
		for(Integer index = 0; index < totalRecords; index ++){
			SObject newRecord = objectRef.newSobject();
			
			for(FieldSettings fSettings : fieldsList){
				newRecord.put(fSettings.fieldName, fSettings.optionalValues.get(Integer.valueof((Math.random() * fSettings.optionalValues.size()))));
			}
		
			for(Schema.DescribeFieldResult dfr : requireFields){
				if(newRecord.get(dfr.getName()) == null){
			
					if(dfr.getType() == Schema.DisplayType.Picklist || dfr.getType() == Schema.DisplayType.MultiPicklist){                      
						newRecord.put(dfr.getName(), dfr.getPicklistValues().get(Integer.valueof((Math.random() * dfr.getPicklistValues().size()))).getValue());
					}
					else if(dfr.getType() == Schema.DisplayType.String || dfr.getType() == Schema.DisplayType.TextArea || dfr.getType() == Schema.DisplayType.URL){
						newRecord.put(dfr.getName(), 'test1');
					}
					else if(dfr.getType() == Schema.DisplayType.Integer || dfr.getType() == Schema.DisplayType.Long || dfr.getType() == Schema.DisplayType.Double){
						newRecord.put(dfr.getName(), 1);
					}
					else if(dfr.getType() == Schema.DisplayType.Date || dfr.getType() == Schema.DisplayType.DateTime){
						newRecord.put(dfr.getName(), System.today());
					}
					else if(dfr.getType() == Schema.DisplayType.Boolean){
						newRecord.put(dfr.getName(), false);
					}
					else if(dfr.getType() == Schema.DisplayType.REFERENCE) {
					
						set<String> sTypes = new set<String>();
						
						for(Schema.sObjectType refTo : dfr.getReferenceTo()){
							sTypes.add(refTo.getDescribe().getName());
						}
						
						if(sTypes.contains('User')){
							newRecord.put(dfr.getName(), UserInfo.getUserId());
						}
						else if(sTypes.contains('Profile')){
							//Special logic for profile as we cannot create profiles will use the profile of the running user
							newRecord.put(dfr.getName(), UserInfo.getProfileId());
						}
					}
					else{
						//Unsupported type is require - need to improve the code
						System.debug('Require field is not populated: ' + dfr.getName() + ' ' + dfr.getType());
					}
				}
			}
			
			newRecords.add(newRecord);
		}
		
		

		insert newRecords;
	}
	
	
	public class FieldSettings{
		private String fieldName;
		private List<object> optionalValues;
		
		public FieldSettings(String fieldName, List<object> optionalValues){
			this.fieldName = fieldName;
			this.optionalValues = optionalValues;
		}
	}
}



Now the usage can be:

DataCreator.generateRandomData(

'Lead', 5, new List<DataCreator.FieldSettings>{

new DataCreator.FieldSettings('Rating', new List<object>{'Hot', 'Warm'}),

new DataCreator.FieldSettings('Country', new List<object>{'Israel', 'Spain', 'US'}),

new DataCreator.FieldSettings('Status', new List<object>{'Open'})});




There is undoubtedly room for improvement. Support for extra field types, for instance, or more flexible options for data settings. I advise carrying it out as necessary.


One change that I would make is to have the method return both the list of new records and a parameter that lets us choose whether or not to insert the data at all. This way, whoever executes the function can easily extend data that the function generate.


public with sharing class DataCreator{
    public static List<sObject> generateRandomData(
        String objectType, 
        Integer totalRecords,
        List<FieldSettings> fieldsList,
        Boolean doInsert){
    
        List<sObject> newRecords = new List<sObject>();

        //Get all object fields
        SObjectType objectRef = Schema.getGlobalDescribe().get(objectType);
        Map<String, Schema.SObjectField> fieldMap = objectRef.getDescribe().fields.getMap();
        List<Schema.DescribeFieldResult> requireFields = new List<Schema.DescribeFieldResult>();
        
        for(Schema.SObjectField sField : fieldMap.values()){
            Schema.DescribeFieldResult dfr = sField.getDescribe();
            
            if(! dfr.isNillable() && dfr.isCreateable()){
                requireFields.add(dfr);
            }
        }
        
        for(Integer index = 0; index < totalRecords; index ++){
            SObject newRecord = objectRef.newSobject();
            
            for(FieldSettings fSettings : fieldsList){
                newRecord.put(fSettings.fieldName, fSettings.optionalValues.get(Integer.valueof((Math.random() * fSettings.optionalValues.size()))));
            }
        
            for(Schema.DescribeFieldResult dfr : requireFields){
                if(newRecord.get(dfr.getName()) == null){
            
                    if(dfr.getType() == Schema.DisplayType.Picklist || dfr.getType() == Schema.DisplayType.MultiPicklist){                      
                        newRecord.put(dfr.getName(), dfr.getPicklistValues().get(Integer.valueof((Math.random() * dfr.getPicklistValues().size()))).getValue());
                    }
                    else if(dfr.getType() == Schema.DisplayType.String || dfr.getType() == Schema.DisplayType.TextArea || dfr.getType() == Schema.DisplayType.URL){
                        newRecord.put(dfr.getName(), 'test1');
                    }
                    else if(dfr.getType() == Schema.DisplayType.Integer || dfr.getType() == Schema.DisplayType.Long || dfr.getType() == Schema.DisplayType.Double){
                        newRecord.put(dfr.getName(), 1);
                    }
                    else if(dfr.getType() == Schema.DisplayType.Date || dfr.getType() == Schema.DisplayType.DateTime){
                        newRecord.put(dfr.getName(), System.today());
                    }
                    else if(dfr.getType() == Schema.DisplayType.Boolean){
                        newRecord.put(dfr.getName(), false);
                    }
                    else if(dfr.getType() == Schema.DisplayType.REFERENCE) {
                    
                        set<String> sTypes = new set<String>();
                        
                        for(Schema.sObjectType refTo : dfr.getReferenceTo()){
                            sTypes.add(refTo.getDescribe().getName());
                        }
                        
                        if(sTypes.contains('User')){
                            newRecord.put(dfr.getName(), UserInfo.getUserId());
                        }
                        else if(sTypes.contains('Profile')){
                            //Special logic for profile as we cannot create profiles will use the profile of the running user
                            newRecord.put(dfr.getName(), UserInfo.getProfileId());
                        }
                    }
                    else{
                        //Unsupported type is require - need to improve the code
                        System.debug('Require field is not populated: ' + dfr.getName() + ' ' + dfr.getType());
                    }
                }
            }
            
            newRecords.add(newRecord);
        }
        
        
        if(doInsert){
            insert newRecords;
        }
        
        return newRecords;
    }
    
    
    public class FieldSettings{
        private String fieldName;
        private List<object> optionalValues;
        
        public FieldSettings(String fieldName, List<object> optionalValues){
            this.fieldName = fieldName;
            this.optionalValues = optionalValues;
        }
    }
}















Retire of Permission on Profiles

If you are working as a Salesforce admin/developer you've probably heard somewhere that Salesforce is planning to make a significant cha...