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.
- It is tedious to type
- 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
- If you need to incrementally add new field verifications, you only need to add a new
withXXX
method to thetheDomainObjectVerify
class.
Some possible extensions
- Verify a batch of records in a single assert, with the errors indexed by the position in the list