“
Test-driven development (TDD) is a software development technique that uses short development iterations based on pre-written test cases that define desired improvements or new functions. Each iteration produces code necessary to pass that iteration's tests. Finally, the programmer or team refactors the code to accommodate changes. A key TDD concept is that preparing tests before coding facilitates rapid feedback changes. Note that test-driven development is a software design method, not merely a method of testing. Test-Driven Development is related to the test-first programming concepts of Extreme Programming, begun in 1999, but more recently is creating more general interest in its own right. Programmers also apply the concept to improving and debugging legacy code developed with older techniques.” - Wikipedia (
http://en.wikipedia.org/wiki/Test-driven_development)
"
Testing is the key to successful long term development. Salesforce.com strongly recommends that you use a test-driven development process, that is, development that occurs at the same time as code development." - Apex Language Reference
I'm sure many of you are familiar with test-driven development. This thread is intended to be a primer for those who may not have experience with it. In the most basic sense, test-driven development involves the automation of code execution in parallel with the development of the code providing the functionality desired. In other words, code is written to call application code and verify the results. This happens alongside software development and should occur during if not before the software is developed.
Here's an example from an HTTP callout class I wrote. The code details are not important but the automated execution in the test class that follows is:
public class HTTPManager
{
// Class vars
public String var1, var2, var3, etc., etc.
public HTTPManager()
{
// Constructor code removed
}
public HTTPManager(String credName)
{
// Overloaded constructor
if(credName == 'TestMethod')
{
isTest = true;
}
}
private void fc(Credentials__c cc)
{
if(cc != null)
{
APIPassword = cc.APIPassword__c;
SiteId = cc.SiteId__c;
EmailServer = cc.EmailServer__c;
MasterListId = cc.MasterListId__c;
InputHeader = '<SITE_ID>' + SiteId + '</SITE_ID>' +
'<MLID>' + MasterListId + '</MLID>' +
'<DATA type="extra" id="password">' + APIPassword + '</DATA>';
}
// Else no credentials in org
}
public HttpResponse HTTPRequest(Request lr)
{
HTTPResponse RetVal;
HTTPRequest req = new HttpRequest();
String endpoint, body;
endpoint = EmailServer + '/mailing_list.html?type=' + lr. Type + '&activity=' + lr. Activity;
body = 'input=<DATASET>' + InputHeader;
for(String inp : lr.Input)
{
body += inp;
}
body += '</DATASET>';
// For display or exception information
Endpoint = endpoint;
Body = body;
Method = lr.HTTPMethod;
// Make the HTTP callout
req.setEndpoint(endpoint);
req.setBody(body);
req.setMethod(lr.HTTPMethod);
HTTP http = new HTTP();
if(!isTest)
RetVal = http.send(req);
return RetVal;
}
public List< ResponseRecord> APIParse(String xml, String funcType, String funcActivity)
{
XmlStreamReader reader = new XmlStreamReader(xml);
List< ResponseRecord> rr = new List< ResponseRecord>();
String currentElementName;
String eventType;
Boolean isInRecord = false;
Boolean initPass = true;
String currentName = '';
String currentEvent = '';
ResponseRecord rr;
if(funcType == 'demographic' && funcActivity == 'query-enabled')
{
XMLDom dom = new XMLDom(xml);
XMLDom.Element element = new XMLDom.Element();
List<XMLDom.Element> records = dom.getElementsByTagName('RECORD');
for(XMLDom.Element r : records)
{
rr = new ResponseRecord();
List<XMLDom.Element> e = r.childNodes;
system.debug('SYSTEM.DEBUG---------->');
system.debug('SYSTEM.DEBUG----------> BEGIN RECORD');
system.debug('SYSTEM.DEBUG---------->');
Integer i = 0;
for(XMLDom.Element ee : e)
{
List<String> aValues = ee.attributes.values();
system.debug('SYSTEM.DEBUG----------> NODE NAME: ' + aValues[0]);
system.debug('SYSTEM.DEBUG----------> NODE VALUE: ' + ee.nodeValue);
if(aValues[0] == 'name')
{
rr. Name = ee.nodeValue;
}
else if(aValues[0] == 'id')
{
rr. Id = ee.nodeValue;
}
else if(aValues[0] == 'type')
{
rr. Type = ee.nodeValue;
}
}
rr.add(rr);
system.debug('SYSTEM.DEBUG---------->');
system.debug('SYSTEM.DEBUG----------> END RECORD');
system.debug('SYSTEM.DEBUG---------->');
}
}
return rr;
}
}
The key elements of test driven development are entry points and branch logic. Calling all functions is a good start but all logic must be tested too. Therefore all conditional logic must be primed and tested via the test code. The goal is to execute 100% of your code via test classes or test functions. Here's the test class I wrote yesterday that accomplishes this (some info changed for obvious reasons):
public class HTTPManager_Test
{
public static testMethod void HTTPManager_Test()
{
Credentials__c c = new Credentials__c(Name = 'TestMethod', APIPassword__c = 'password', SiteId__c = '666333', EmailServer__c = 'https://www.FFF.com/API', MasterListId__c = '333');
insert c;
HTTPManager hm = new HTTPManager('TestMethod');
HTTPManager.Request lr = new HTTPManager.Request();
lr.HTTPMethod = 'POST';
lr.Type = 'record';
lr.Activity = 'query-listdata';
lr.Input = new List<String>();
lr.Input.add('<DATA type="extra" id="type">active</DATA>');
HttpResponse hr = hm.HTTPRequest(lr);
String xml = '<DATASET ><TYPE >success</TYPE><RECORD ><DATA type=\'name\' >EMAIL_ADDRESS</DATA>;
hm.APIParse(xml, 'demographic', 'query-enabled');
hm = new HTTPManager();
}
}
The above example instantiates the HTTPManager class and populates it with various data to ensure that all lines of code are executed. You'll notice that I call it again at the end with no parameters causing the overloaded constructor to fire.
The Force.com platform supports test-driven development and it's included in the Eclipse plug-in. You simply right-click on your test class and select “Force.com | Run tests.” Test-driven development is no guarantee that all bugs will be discovered prior to release. However, with these automated processes in place, you're far more likely to identify problems without relying on unit and regression tests. When functionality is added to an existing application you can easily identify any potential issues with legacy code by re-running these tests to verify that nothing was broken.
At first this may seem cumbersome and even unnecessary. But it improves code manageability on so many levels that I'll never return to coding without it.
