Builder pattern – in testmethod asserts

A common problem in business applications is to test multiple field values set by some service in an SObject. Normally, you might start down via this approach:

// ...code that updates some Opportunity

// now verify against expected values
Opportunity oActual = [select amount, closeDate, stagename from Opportunity where ...];
System.assertEquals(1000.0, oActual.amount);
System.assertEquals(Date.newIstance(2020,1,1), oActual.closeDate);
System.assertEquals('Closed Won',oActual.Stagename);

But this has several problems.

  1. It is tedious to type
  2. The testmethod stops on the first error yet other errors may be lurking. This is especially true if you have to verify many SObject fields. So, you end up running a test, finding an error, then fixing, then rerunning, then exposing a new error. Rinse and repeat and most likely, your attention has wandered into social media before too long.

Thanks to my colleagues Adrian Larson, sfdcfox and Keith C on Salesforce Stackexchange, I was introduced to the Builder Pattern. I decided to apply it to this issue.

Objective
I was looking to get all field verify errors for an SObject exposed in a single System.assert. So, the goal was to code this:

System.assertEquals(SObjectVerify.NO_ERRORS,OpportunityVerify.getInstance(oActual)
  .withAmount(60000)
  .withStage('Closed Won')
  .withCloseDate(Date.newINstance(2020,1,1)
  .results());

that, if it doesn’t verify, displays the assertion failure as:

System.AssertException: Assertion Failed: Expected: , Actual:
AccountId Expected: 00540000000wbFSAAY vs. Actual: 0014000000HQGCMAA5
CloseDate Expected: null vs. Actual: 2006-10-14 00:00:00

So, here goes the code – one base (super) class that does all the work and one , each, domain (SObject-specific) class.

The base (super) class

public abstract class SObjectVerify {
	
	//	-------------------------------------
	//	Inner Class to track Variances between 
	//	expected and actual values for a given field
	//	-------------------------------------
	private class Variance {
		Object				actVal;
		Object				expVal;
		
		private Variance(Object expVal, Object actVal) {
			this.expVal		= expVal;
			this.actVal		= actVal;
		}
	}
	private map<Schema.SobjectField,Variance> fldToVarianceMap = new map<Schema.SobjectField,Variance>();
	
	public static final String NO_ERRORS	= '';	// Used by caller in system.assert as expectedResult
	
    private Sobject	actSobj;	// the actual Sobject
    
    // If all expected values match all actuals, 
    //	return NO_ERRORS, otherwise, return a line-broken string of variances
    protected String getResults() {
    	String res 		= NO_ERRORS;
    	for (Schema.SobjectField fld : fldToVarianceMap.keySet())
    		res += '\n   ' + fld + ' Expected: ' + fldToVarianceMap.get(fld).expVal + 
    			     ' vs. Actual: ' + fldToVarianceMap.get(fld).actVal;
    	return res;	
    }

	//	(super) constructor
	public SobjectVerify(SObject actSobj) {this.actSobj = actSobj;}
	
	
	//	Builder pattern, returns ourselves after comparing 
	//	the actualFldVal vs expectedFldVal, stashing variances in a map
	protected SObjectVerify verify(Object expVal, Schema.SObjectField fld) {
		Object actVal	= this.actSobj.get(fld);
		if (expVal == null) {
			if (actVal != expVal)
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf Blob) {
			if ((Blob) actVal != (Blob) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf Boolean) {
			if ((Boolean) actVal != (Boolean) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf Date) {
			if ((Date) actVal != (Date) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf DateTime) {
			if ((DateTime) actVal != (DateTime) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf Decimal) {
			if ((Decimal) actVal != (Decimal) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf ID) {
			if ((ID) actVal != (ID) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf Integer) {
			if ((Decimal) actVal != (Integer) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		else
		if (expVal instanceOf String) {
			if ((String) actVal != (String) expVal) 
				this.fldToVarianceMap.put(fld,new Variance(expVal,actVal));
		}
		return this;												
	}
}

The domain (Sobject-specific) Class

public class OpportunityVerify extends SObjectVerify {
	
	//	Usage
	//	System.assertEquals(SObjectVerify.NO_ERRORS,OpportunityVerify.getInstance(someActualOpportunity)
	//							.withXXX(someExpectedValFldXXX)
	//							.withYYY(someExpectedValFldYYY)
	//							.results();
	
	//	If the assertion fails, System.assert displays for each field at variance (separated by \n):
	//
	//		fldXXX expected: .... vs. actual: ....
	//		fldYYY expected: .... vs. actual: ....
	
	private SObject actSObj;   // actual Opportunity, to be compared with Expected Opportunity
	
	public OpportunityVerify withAccountId(Object expVal) 	{
           return (OpportunityVerify) verify(expVal,Opportunity.AccountId);
        }
	public OpportunityVerify withAmount(Object expVal) 	{
           return (OpportunityVerify) verify(expVal,Opportunity.Amount);
        }
	public OpportunityVerify withCampaignId(Object expVal) 	{
           return (OpportunityVerify) verify(expVal,Opportunity.CampaignId);
        }
	public OpportunityVerify withCloseDate(Object expVal) 	{
           return (OpportunityVerify) verify(expVal,Opportunity.CloseDate);
        }
	public OpportunityVerify withHasOli(Object expVal) 	{
           return (OpportunityVerify) verify(expVal,Opportunity.HasOpportunityLineItem);
        }
	public OpportunityVerify withStage(Object expVal) 	{
           return (OpportunityVerify) verify(expVal,Opportunity.StageName);
        }
	
	public static OpportunityVerify	getInstance(SObject actSobj) {
		return new OpportunityVerify(actSobj);
	}
	
	public String results() {
		return super.getResults(); // super class returns either NO_ERRORS (empty string) or a single string of variances)
	}
	
	public OpportunityVerify(SObject actualSobj) {
		super(actualSobj);
	}
}

Additional advantages

  1. If you need to incrementally add new field verifications, you only need to add a new withXXX method to the theDomainObjectVerify class.

Some possible extensions

  1. Verify a batch of records in a single assert, with the errors indexed by the position in the list

One thought on “Builder pattern – in testmethod asserts

  1. Luke Freeland

    Cropredy,

    Your blog is nice and this pattern is really nice too. I especially like the “multiple assertion” capability. Will check out other posts too.

    Some other suggestions:

    1) In the constructor, do a null check on the SObject record so that you don’t have to do null checks later to ensure the SObject is instantiated.
    2) Allow an optional assertion message so the caller can provide additional contextual information as needed to help troubleshooting.
    3) Allow for other assertion comparison such as contains, startsWith, etc.

    Happy Coding,
    Luke

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *