Monthly Archives: March 2016

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

Batchables-Limits Exceptions

A bad thing happened the other day. Here was the sequence:

  1. Batch job started. start() method returned 5000 + Opportunity rows.
  2. Database.stateful used to record an internal log of activity for subsequent posting in the finish() method.
  3. Each batch of 200 was passed to execute() method.
  4. Execute method added bits to the stateful log (a string variable).
  5. Batch 21 (out 28) blew up on a Limits Heap Size exception. Blow up continued on batches 22-28.
  6. finish() method started and took value from Database.Stateful variable and persisted to Sobject Log__c (s).
  7. AND HERE IS WHERE BAD THINGS HAPPENEDfinish() method started a “finalize” batch job, passing a list of sobject IDs that had exceptions in any previous batch execute. The finalize batch job (i.e. the chained batch job), made updates to all Opportunities that weren’t selected from the previous batches start() method and weren’t already marked as exceptions. In my case, these Opportunities were marked as closed lost.

So .. because the Opportunities in batches 21-28 were never processed and never marked with an exception (because of the uncatchable Limits exception), the chained (second) batch job blithely assumed that the Opportunities in batches 21-28 had never been fetched in the previous batch job’s start() method. Hence, perfectly good Opportunities got marked as closed lost.

Uh-oh.


So, what should I have done differently?

First, I wrongly assumed that a Limits exception would terminate the entire batch job, not just the currently running execute() batch.

And, since of this misconception, the finish() method unconditionally executes without knowing if all of the batches passed without uncaught exceptions. And, any work the finish() method performs that involves DML-type work, including scheduling a subsequent chained job may lead to incorrect behavior.

  1. The finish() method has access to the BatchableContext and can get, via getJobId(), the AsyncApexJob that represents the batch job.
  2. AsyncApexJob has a field NumberOfErrors that identifies how many batches are in error. If greater than zero, appropriate business logic should be applied.

Of course, the Limits Exception needs to be avoided in the first place by taking a different approach to stateful logging. I’ll investigate this in a subsequent post (but don’t stay up waiting for it!)

Download multiple ContentVersion files as zip

In our org, there is a relatedList underneath the Contract object of ContentVersions of the contract and any amendments. Users wanted to be able to download all (or download selected) files with one click.

Such relatedList does not provide any download button. You need to select each file in turn, go to the Content page and click Download. Tedious. Users hated it. I hated it.

Googling revealed an interesting post on downloading attachments into a zip using JSZip. However, we had files greater than 15MB and I worried about Heap Limits.

Turns out SFDC provides download ContentVersion as zip already on the Content tab. (Where our users nor I almost never go as the Contract relatedList is more convenient). So, I used the browser developer Tools to see what URL was constructed by SFDC and saw that it exploited the undocumented servlet.shepherd.

The specific URL to use is:

{!URLFOR('/sfc/servlet.shepherd/version/download/' & delimitedSelectedIdList &'?')}

where delimitedSelectedIdList is a list of ContentVersion ids separated by forward slash. from my understanding, the ids separated by slashes is a sometimes-used REST convention to specify a list of resources.

Example (downloads 2 files into single zip)

{!URLFOR('/sfc/servlet.shepherd/version/download/068xxxxxxxxxxxxxxx/068yyyyyyyyyyyyyyy?')}

I tested this on large zip files (~400 MB) and had no issues.

Notes:

  1. servlet.shepherd is undocumented and hence unsupported so this might not be the right answer for a mission critical application.
  2. servlet.shepherd does not work on Attachments, only ContentVersion.

Sites Unauthorized error displaying VF page. Why?

One of my users reported getting an ‘Unauthorized Error’ after clicking a button on a Sites Visualforce page. What could be the reason for this?

Well, you might think that the obvious reason would be that the VF page wasn’t included as an allowed page in the Sites Guest user profile. This is easy to check by going to:

Setup | Develop | Sites | mysite | Visualforce Pages

But, the page was enabled. Hmm.

I recreated the error and examined the debug logs. Perhaps the controller was throwing an exception. Remember, you have to monitor the sites guestuser, not yourself, to see the debug logs. But, no controller exception.

I looked through the change history and noted the controller had been updated recently so it must have had something to do with a relatively recent Story.

And the answer was…..

While the controller had been updated (certain getters had their names changed), the VF page hadn’t been updated to reflect the new getter properties. The developer had neglected to test the VF page and had a test been done, the “unauthorized error” would have surfaced and the change wouldn’t have been pushed into production [the developer was me 🙁 ]

Here what was happening:

  1. At run time, clicking the button requested VF to serve up Foo.page.
  2. VF does a runtime compile of the page
  3. The compile fails because the page references getters on the controller that no longer exist
  4. Sites default error handling ensues. In our case, this displays the Out-of-the-box unauthorized page

User Debug Logs Missing in Eclipse IDE – Why?

I had recently upgraded my Eclipse IDE to V36 from V33. The Eclipse Workspace pointed to two different orgs:

  • Developer Edition
  • Sandbox for a client’s PROD org

When I ran a Test Configuration for the Developer Edition, the user debug logs appeared in the Apex Test Results pane. But, for the sandbox org, while the tests ran, the user and system debug log panes were empty. Nothing. Zippo. Bupkus. The logs appeared in the Developer Console but not within the Eclipse IDE.

Solution
Eclipse had prompted me to upgrade the project to V36 but I had ignored that message. Silly me. By upgrading the project to the same version as the Eclipse IDE, and rerunning the tests, the debug logs appeared in the Eclipse Apex Test Results pane.

Eclipse system debug log

Eclipse user debug log