Developing For Optional Salesforce Features
Writing code to be distributed via a managed package is a very different ball game to writing code for a single production org, and the primary reason for that is that you can't make any assumptions about the configuration (or even edition) of the org that your package will be installed into.
For starters your test methods have to be able to deal with things such as validation rules that you might not be anticipating, which is just one reason why you should look at making your life easier by utilising SmartFactory as I discussed previously. Another area of consternation involves Salesforce features that may or may not be present, and on which you may or may not depend, examples of such are mutli-currency support and record types.
What's The Issue?
Probably the best value app around
Various Salesforce features, such as multi-currency support, record types and the new communities (if you've not checked out the new success community, then do it!), actually modify the data model of an org; they all add extra objects and/or fields. If you've ever worked with record types for instance, you'll be familiar with the field RecordTypeId
which can be found on objects which have add record types added, and there is a RecordType
object which is used to represent the types.
The tricky part comes when you want to use these fields and objects in your application. We recently updated Ring My Bell to properly support organisations that use multiple currencies, and to do so involved ensuring that the value in the CurrencyIsoCode
field on the opportunity was copied to the same field on a custom object we use as part of the package. You'd expect the code to look something like the following:
customObj.CurrencyIsoCode = oppty.CurrencyIsoCode;
... and you'd be right, it'd work as you expect. Then you'd try and install the package into an org which doesn't have multi-currency support and it would fail to install, with the only displayed error being some random number that you'll have to ask support to look up. The problem, simply put, is that field doesn't exist on either object, and so the code is invalid.
What's The Fix?
Dynamic SOQL is your friend here, as so are the Get()
and Put()
methods which are part of the SObject
base class. Essentially there can't be any hard references to a field such as CurrencyIsoCode
or RecordTypeId
and so we have to leverage Strings instead, as a way of creating a level of abstraction.
// A query like this which references CurrencyIsoCode directly
for(Opportunity o : [select Id, CurrencyIsoCode from Opportunity])
// Would be replaced with this, which uses a String to hide the field from the compiler
for(Opportunity o : Database.Query('select Id, CurrencyIsoCode from Opportunity'))
Inside such a loop is where the aforementioned SObject methods come into play (trivial example alert!):
for(Opportunity o : Database.Query('select Id, CurrencyIsoCode from Opportunity'))
{
// Regular syntax is all good for the Id
MyObject__c obj = someMap.Get(o.Id);
// But a String should be used for the field which might not be, on both objects!
obj.Put('CurrencyIsoCode', o.Get('CurrencyIsoCode'));
}
The problem with the above code is that although it doesn't trigger an error at compile time (whoo!), it will cause an issue at run time in orgs that don't have multi-currency enabled (boo!). The solution is simple enough of course, only run code related to a feature if said feature is available. One quick way to test for a specific feature is to simply check whether or not the field you want actually exists; for the above we could simply check the opportunity to see if it has the field or not.
if(Schema.SObjectType.Opportunity.fields.GetMap().Get('CurrencyIsoCode') != null)
{
// We are good for launch...
// Note the use of fields.GetMap() so the field reference is dynamic
}
What's the Catch?
This is great, and it works, yet there is a snag: test coverage. If you use if statements like the above (which you probably will because there's not many other options here!) then the code inside will never get run on orgs which don't have the specific feature enabled, which is of course, what we want. The problem is that code will never get executed when the tests are run either, meaning your code coverage is lower on those orgs than it is for those with the feature enabled.
Right now I don't have a good solution for this, luckily for me the lines of such code are few and far between, and if you inline a one-liner block you can cover the line (you could do more, but it'd look nasty); other than that I'm at a loss.
if(Schema.SObjectType.Opportunity.fields.GetMap().Get('CurrencyIsoCode') != null)
{
// don't do this
}
if(Schema.SObjectType.Opportunity.fields.GetMap().Get('CurrencyIsoCode') != null) { // do this! }
It seems to me that the best solution would be one data model for all (at least for standard objects), but failing that perhaps some specific method/class annotations would do the job. @UsesRecordType
anyone?
Update, 8th July:
Dan Appleman has written a blog post as a follow up to this explaining one way in which you can achieve code coverage for blocks of code like the above. Although I'm not a fan of modifying code specifically to support tests and test coverage, this is one scenarios where it's current unavoidable.