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 the theDomainObjectVerify
class.
Some possible extensions
- Verify a batch of records in a single assert, with the errors indexed by the position in the list