Tag Archives: apex

A Pattern for Apex Test Code So You Can Forget About Required Fields

Published by:

I recently had the opportunity to start writing some code in a brand new org, which got me thinking about the best way to do test object creation. You know, that annoying problem where you need to generate an Account in a test class about 77 times. Here’s a pattern I came up with.

This method involves some overhead but it has the advantages of (1) allowing you to set defaults for certain objects so you don’t have to worry about them – for some objects create them with all defaults with just one line, and (2) a generic, repeatable pattern.

It’s based on an abstract class that implements the base operations using some generic sObject magic outlined here.

A lot of code in this post, bear with me.

public abstract class TestObjectFactory{

    public abstract Schema.SObjectType getSObjectType();
    public abstract String getSObjectAPIName();
    List<Test_Class_Default__mdt> defaults = new List<Test_Class_Default__mdt>();
private Map<SobjectField,Object> values = new Map<SobjectField,Object>();

    public void setFieldValue(sObjectField field,Object value)
    {
    	values.put(field,value);
    }

    public void clearFieldValues()
    {
    	values = new Map<SobjectField,Object>();
    }

   public SObject createObject( Map<SObjectField, Object> valuesByField, Boolean doInsert) {
        // Initialize the object to return
        SObject record = this.getSObjectType().newSObject(null, true);

        // Fill defaults 
        record = fillDefaults(record,false);

        // Populate the record with values passed to the method
        if (valuesByField != null)
	        for (SObjectField eachField : valuesByField.keySet()) {
	            record.put(eachField, valuesByField.get(eachField));
	        }
        if (doInsert)
        	insert record;
        // Return the record
        return record;
    }

    // Overload version that just uses defaults
	public SObject createObject( Boolean doInsert) {
        // Initialize the object to return
        SObject record = this.getSObjectType().newSObject(null, true);
        // Fill defaults 
        record = fillDefaults(record, false);
        if (doInsert)
        	insert record;
        return record;
    }
    public void updateObject(sObject record, Map<SObjectField, Object> valuesByField)
    {
    	if (record.Id == null)
    		throw new MyException('Test Object Factory: Attempt to Update a Record with a Null ID');
       	for (SObjectField eachField : valuesByField.keySet()) {
            	record.put(eachField, valuesByField.get(eachField));
        }
        update record;

    }

    public SObject upsertObject(sObject record, Map<SObjectField, Object> valuesByField)
    {
    	for (SObjectField eachField : valuesByField.keySet()) {
            record.put(eachField, valuesByField.get(eachField));
        }
        upsert record;
        return record;
    }

    public void deleteObject(sObject record)
    {
    	String recordId = record.Id;
    	database.delete(recordId);
    }
}

For each object you will use, you need to create a class that implements the abstract factory class.

public class TestObjectFactoryAccount extends TestObjectFactory{

    public override Schema.SObjectType getSObjectType(){
        return Account.SObjectType;
    }
   
    public override String getSObjectAPIName(){
        return 'Account';
    }
    
    public Account createRecord(Map<SObjectField, Object> valuesByField, Boolean doInsert){
        return (Account) createObject(valuesByField, doInsert);
    }
    
    public Account createRecord(Boolean doInsert){
        return (Account) createObject( doInsert);
    }
    
    public void updateRecord(sObject record, Map<SObjectField, Object> valuesByField){
        updateObject(record, valuesByField);
    } 
    
    public SObject upsertRecord(sObject record, Map<SObjectField, Object> valuesByField){
        return (Account) upsertObject(record, valuesByField);
    }
    
    public void deleteRecord(sObject record)
    {
    	deleteObject(record);
    }

}

Then, in your test code you would need to instantiate the factory for each object, and set values in the Map using setFieldValue.

TestObjectFactoryContact contactFactory = new TestObjectFactoryContact();
		contactFactory.setFieldValue(Contact.Role__c, contactRole);
		contactFactory.setFieldValue(Contact.AccountId,childAccount.Id);
		contactFactory.setFieldValue(Contact.LastName,'child Contact');
		contactFactory.setFieldValue(Contact.Job_Level__c, contactLevel);
		Contact childContact = contactFactory.createRecord(contactValues,true);

NB: You will need to be careful about the Map (setFieldValue calls), since in all subsequent calls the values class variable will still contain its prior values. That could cause logic issues if you’re setting fields you didn’t intend to. The abstract class contains a clearFieldValues() method to clear it out if necessary.

Seems like.. That’s a Lot of Work For Not a Lot of Benefit?

No doubt, it’s wordy. But here’s why it’s worth it. The big benefit is that all your defaults/required fields are taken care of – if you had a required field on your Contact, the code would pre-fill it for you. Or, you can override the default by setting the value in the Map.

I created a custom metadata type that allows you to fill in defaults; if you look in the abstract class above, it calls a fillDefaults() method. This goes into the custom metadata and populates the required fields. For example, you can set the default Name for Accounts.

The setDefault method looks at this, and if you don’t override it, sets the Name field automatically on every record the test generator makes.

Now, in your abstract class, add this method:

public sObject fillDefaults(sObject record, Boolean isTest)
    {
    	// Public so TestObjectFactoryTest can see it.
		String objectName;
		if (isTest) 
			objectName = 'TestObjectFactoryTest';
		else
			objectName = getSObjectAPIName();
		SObjectType objectType = getSObjectType();
		String testName;

		if (defaults.size() == 0)
		{
	    	defaults = 
	    		[SELECT 
	    			Checkbox_Value__c
	    			,DateTime_Value__c
	    			,Date_Value__c
	    			,Email_Value__c
	    			,Field__c
	    			,Number_Value__c
	    			,Percent_Value__c
	    			,Phone_Value__c
	    			,Picklist_Value__c
	    			,Text_Area_Value__c
	    			,Text_Value__c
	    			,Type__c
	    			,URL_Value__c
	    			FROM Test_Class_Default__mdt
	    			WHERE Object__c = :objectName
	    			AND Field__c != null];
	    		}
    	
    	for (Test_Class_Default__mdt d : defaults)
    	{
    		Object value = null;
    		if (d.Type__c == 'Checkbox')
    			value = d.Checkbox_Value__c;
    		if (d.Type__c == 'Date')
    			value = d.Date_Value__c;
    		if (d.Type__c == 'DateTime')
    			value = d.DateTime_Value__c;		
	    	if (d.Type__c == 'Email')
	    		value = d.Email_Value__c;
	    	if (d.Type__c == 'Number')
	    		value = d.Number_Value__c;
	    	if (d.Type__c == 'Percent')
	    		value = d.Percent_Value__c;
	    	if (d.Type__c == 'Phone')
	    		value = d.Phone_Value__c;
	    	if (d.Type__c == 'Picklist')
	    		value = d.Picklist_Value__c;
	    	if (d.Type__c == 'Text')
	    		value = d.Text_Value__c;
	    	if (d.Type__c == 'Text Area')
	    		value = d.Text_Area_Value__c;
	    	if (d.Type__c == 'URL')
	    		value = d.URL_Value__c;

	    	// Get all the fields from the Object as sObjectField records
	    	// Then set the sObjectField value as a plain Object
	    	Map<String,Schema.SObjectField> mFields = objectType.getDescribe().fields.getMap();

	    	try {
	    		if (value != null && !isTest)
	    		{
	    			record.put(mFields.get(d.Field__c.toLowerCase()),value);
	    		}
	    		else
	    			testName = d.Text_Value__c;
	    	}
	    	catch (System.Exception e){
	    		String error = 'TestObjectFactory: Unable to set default field ' + d.Field__c + ' to value ' + value + ' On Object ' +objectName + ' Error:' + e.getMessage();
	    		System.debug(error);
	    		throw new MyException(error);
	    	}
   	}
    	if (isTest)
    		return new Account(Name = testName);
    	else
    		return record;
}

The non-insert use cases are probably less valuable, but it least provides a consistent interface. For objects that don’t require other objects (like a parent Account) you can just use the factory.createObject(true); which will create the entire object, insert it, and return it to you.

Test Code Implications

Since this is an ordinary non-test class (has to be, since abstract is not available when @isTest) you need to create some test code to verify it actually works, and to maintain coverage requirements. My test class has a short and quick way to test each piece, for coverage purposes. If you do it carefully, you can set so you can find/replace the object name (i.e., change ‘Account’ to ‘Contact’) which can get you 80% of the way there.

@isTest
private class TestObjectFactoryTest {

	@isTest static void AccountTest() {
		List<Account> accounts = new List<Account>();

		Test.startTest();
		TestObjectFactoryAccount factory = new TestObjectFactoryAccount();
		system.assertEquals(factory.getSObjectAPIName(),'Account');
		system.assertEquals(factory.getSObjectType(),Account.SobjectType);

		// Create
		factory.clearFieldValues();
		factory.setFieldValue(Account.Name,'Test');
		Account record = factory.createRecord(true,true);
		accounts = [SELECT Id, Name From Account WHERE Id = :record.Id];
		system.assertEquals(accounts.size(), 1);
		system.assertEquals(accounts[0].Name,'Test');

		// Overloaded Create
		record = factory.createRecord(true);
		Accounts = [SELECT Id, Name From Account WHERE Id = :record.Id];
		system.assertEquals(Accounts.size(), 1);

		// Update
		factory.setFieldValue(Account.Name,'Update Test');
		factory.updateRecord(record);
		accounts = [SELECT Id, Name From Account WHERE Id = :record.Id];
		system.assertEquals(accounts.size(), 1);
		system.assertEquals(accounts[0].Name,'Update Test');

		//Upsert with Insert
		Account upsertInsert = factory.createRecord(true);
		factory.setFieldValue(Account.Name,'Upsert Insert Test');
		record = (Account) factory.upsertRecord(upsertInsert);
		accounts = [SELECT Id, Name From Account WHERE Id = :record.Id];
		system.assertEquals(accounts.size(), 1);
		system.assertEquals(accounts[0].Name,'Upsert Insert Test');

		// Upsert with Update		
		factory.setFieldValue(Account.Name,'Upsert Update Test');
		record = (Account) factory.upsertRecord(record);
		accounts = [SELECT Id, Name From Account WHERE Id = :record.Id];
		system.assertEquals(accounts.size(), 1);
		system.assertEquals(accounts[0].Name,'Upsert Update Test');


		// Double check that we have 3 records now
		accounts = [SELECT Id, Name From Account ];
		System.assertEquals(accounts.size(), 3);

		// Delete
		factory.deleteRecord(record);
		accounts = [SELECT Id, Name From Account ];
		System.assertEquals(accounts.size(), 2);

		System.assertEquals(Account.SObjectType, factory.getSobjectType());
		System.assertEquals(factory.getSObjectAPIName(),'Account');

		Test.stopTest();
	}

Like it? Hate it? Like I said, it’s wordy, but consistent, repeatable, and eliminates the need to think about default values. Feedback welcome below, or at @BobHatcher.Demand_Unit_Persona__cDemand_Unit_Persona__c

Salesforce: Check if Record Has Been Manually Edited

Published by:

We recently had a requirement in Salesforce that was a little unique. An ongoing automated process would update the associated Contact to a record, however they didn’t want the automated process to do the update if the field had been updated by a human. Makes sense – if someone takes the time to make a manual association of a Contact to a record, then it’s probably better information than the automatic matching algorithm.

As far as I can tell it’s impossible at runtime to know if the edit is due to a UI edit or is coming from code, so that’s out.

Here’s how we made it happen. The idea is to have a “last updated by code” checkbox that will be set by a workflow. But in order for it to know if it’s been updated by code, you need a 2nd checkbox that is only updated in Apex.

First, make two checkboxes on your record:
Contact_Change_Trailing_Lock__c
Contact_Last_Changed_By_Code__c

Set the latter to true by default when a new record is created.

When your code updates the record, set Contact_Change_Trailing_Lock__c = true.

Then, make two workflows.
if(and(Contact_Change_Trailing_Lock__c=false,ischanged(Contact__c)),true,false)
-> Field Update – Contact_Last_Changed_By_Code__c = false

And
if(and(ischanged(Contact_Change_Trailing_Lock__c),Contact_Change_Trailing_Lock__c=true),true,false)
–> Field Update: Contact_Last_Changed_By_Code__c = true
–> Field Update: Contact_Change_Trailing_Lock__c = false

This results in you being able to use the Contact_Last_Changed_By_Code__c checkbox downstream in your logic to know not to overwrite the manual edit.