Hooray, I wrote my first Apex today!
This is a culmination of my movement up the Salesforce Developer hierarchy — I have finally reached the ‘Apex’ of Developer-hood!
The Challenge
My challenge was to write a trigger to override StageName on Opportunities because the standard object has “issue”. In fact, I’ve noticed that trying to use most standard objects in Salesforce.com runs into a problem at some stage or another. The specific problems I have with Opportunities are:
- My external system does not know anything about data in Salesforce.com. I extract all the data directly from a database and I don’t want to store any Salesforce IDs in that database. Thus, because I don’t know if a record is being updated or created, I have to use Upsert.
- StageName, CloseDate and Name are Required Fields
- If I use Upsert with DataLoader, I must supply all Required Fields every time I load data
- The field values within Salesforce, therefore, get overwritten whenever I Upsert — that is, if the user has changed the StageName or the Name within Salesforce, the values gets overwritten on the next Upsert
- I therefore can’t let the users modify these fields (not particularly helpful!)
The Workaround
So, I decided to let my external system populate StageName, and use a custom field to store the “actual” Status as set by the users. This, however, gives rise to more problems:
- My users want to track Status transitions
- Opportunity is a standard object, so I can’t use History Tracking (that’s only available on Custom objects)
- The Stage History capability on Opportunities only tracks Amount, Probability,
Stage, and Close Date — not Custom fields! (Update: I think this changed under Summer ’08) - I can’t use the normal StageName field due to my DataLoader Upsert requirements
What I needed was the ability to let users change the Stage, but not let the DataLoader update it. So, I created an Apex Trigger that ignores the changes to the StageName field — it literally wouldn’t save those changes. Unfortunately, it started treating updates from DataLoader and users identically, which meant that it would ignore user changes, too. Mmm…
The Solution
I created a before insert Apex Trigger on the Opportunity object. The rules are as follows:
- My external system passes values for StageName with a prefix (eg ‘Stage:Renewed’). This ‘Stage’ prefix indicates that the data has come via DataLoader. This is important because my external system determines whether an Opportunity is actually Closed/Won as a result of a purchase. However, it does not care about “Open” statuses, which are passed through as “Stage:Ignore”.
- The Trigger looks at the StageName value
- If it is prefixed with “Stage:Ignore”, the Trigger resets the StageName to the previous value, preventing the change. This lets me import data with the DataLoader (due to required fields) but then ignore it within Salesforce.com!
- I then have other rules that cater with New records (eg setting a default StageName value) and records where users have changed the value (which I just let through).
The code looks something like:
trigger Ignore_stage on Opportunity (before insert, before update)
{
// Loop through the incoming records
for (Opportunity new_o : Trigger.new) {
// If it is a new opportunity with 'HAMS:Ignore'
if (new_o.StageName.equals('HAMS:Ignore')) {
// New record being inserted?
if (Trigger.isInsert) {
// Set StageName to 'New'
new_o.StageName = 'New';
}
else {
// Keep the old stage name
new_o.StageName = System.Trigger.oldMap.get(new_o.id).StageName;
}
}
else {
// If it comes from HAMS, use the supplied status (strip off the prefix)
if (new_o.StageName.startsWith('HAMS:')) {
new_o.StageName = new_o.StageName.substring(5);
}
}
// Else, don't do anything -- just let the new value through.
}
}
The Testing Bit
I then had the opportunity to play with Testing. Every Apex Class requires Test Coverage within Salesforce.com. I thought this was just Salesforce.com forcing some sort of good programming technique, but then I discovered another benefit. This is from the Force.com Cookbook:
Test methods are used both by developers for debugging purposes and also by Salesforce.com for testing before upgrades to new versions of the platform or Apex.
So, every test is actually used to test future versions of Salesforce.com! I have no idea what would happen if the tests failed, since Salesforce.com don’t officially have access to your data.
Anyway, I had to write some tests for my Trigger. Good thing I did, because I found plenty of logic problems in the code!
As an aside, it’s worth mentioning that the Eclipse environment for developing Apex code (“Force.com IDE”) is extremely useful. It lets you see ‘into’ your Salesforce.com account, with all objects and code visible. Simply edit some code, hit Save and (after a few seconds delay for the round-trip) Salesforce.com gives feedback on syntax errors and test coverage. Very nice, and much more interactive than trying to do it all via text files and a web interface.
The discovery
Testing did not go well. For some reason, my assertions were failing. Here’s my test class:
public class TriggerTest_IgnoreStage {
static TestMethod void testIgnoreStageTrigger() {
Opportunity o = new Opportunity(CloseDate = Date.newInstance(2008, 01, 01),
Name = 'Test Opportunity', StageName = 'HAMS:Ignore');
insert o; // This should set StageName = 'New'
System.assertEquals('New', o.StageName);
}
}
Unfortunately, this kept failing. I even inserted debug code into the Trigger that proved that the StageName was being changed to ‘New’, but each time I received the error:
System.Exception: Assertion Failed: Expected: New, Actual: HAMS:Ignore
It took me ages to realise that I have to reload the object before testing its value:
public class TriggerTest_IgnoreStage {
static TestMethod void testIgnoreStageTrigger() {
Opportunity o = new Opportunity(CloseDate = Date.newInstance(2008, 01,
01), Name = 'Test Opportunity', StageName = 'HAMS:Ignore');
insert o; // This should set StageName = 'New'
// Reload object
Opportunity o2 = [select id, StageName,
Prevous_Renewals__c from Opportunity where Id = :o.Id];
System.assertEquals('New', o2.StageName);
}
}
This worked nicely. I was still a little confused, because o.Id was getting set, but the other field values were not. The Force.com Cookbook doesn’t do a good job of showing Trigger tests — all the tests check exception objects rather than returned values. I don’t think I’m alone — this guy had a similar experience on the Community discussion boards.
Anyway, my Trigger worked in my Sandbox environment, so it was time to migrate it to Production. This is made very simple with the Eclipse plug-in. I told it which files to send across, provided authentication to my production server and… got an error! It told me that I had 0% test coverage. Rather than panicking, I re-ran the tests, got 100% (again) and tried another migration. This time it worked!

My job here is done! Now I can go home.
The Bottom Line
- Apex works, but it has a learning curve
- The Eclipse plug-in for Force.com development is mighty fine!
- Test your code
July 23rd, 2008 at 5:28 pm
Just stumbled upon your blog John – great to see fellow Sydney siders getting into Force.com!
Keep up the blog, its a great read so far!
Regards,
Ryan