Unit testing *sigh*. Oh how they vex me. If they weren’t so important (and required) I’d just skip the lot, but they are and so we – champions of software development – must press on in the face of dreary complexity; we will not back down, we will not surrender, we will look that CRT/LCD screen in the pixels and say, “Untested units, you will not defeat me!”.
Diving into the thick of things, the most common question seems to be, “Why can’t I get code coverage for my entire class?!”. The trick here is to think like a runtime engine, and consider how you might journey through all possible testing paths. Now I never said it’s easy, but with a bit of practice (and 8 truck loads of patience) you’ll get there. Let’s look at some common cases.
The If-Else Interrogation
This situation comes about when we have a piece of code something like this,
[code language=”java”]
public class MyClass{
public Integer var{get;set;}
public void aMethod(){
if(var==1){
// Operations for this value
} else if (var==2){
// Different operations for this value
} else {
// And more operations for all other values
}
}
}
[/code]
If we assume that the variable called ‘var’ is set by a field on a VisualForce page (during normal operation), our test methods have to mimic this behaviour and set the it programmatically. Not only this, but we need to set the variable ‘var’ to each of the values required to step through each branch of the if-else statement. Further to this, I would suggest a test method per value that ‘var’ can assume. Alot of work I know, but easier for our silly human brains to comprehend, as well as simpler for our programmer hands to maintain. Test code for the above might look so,
[code language=”java”]
public static void testMethod testAMethod1(){
MyClass mc = new MyClass();
// Mimic that variable being set by the page
mc.var = 1;
mc.aMethod();
System.assert(…); // Assert that the operations in the first branch worked
}
public static void testMethod testAMethod2(){
MyClass mc = new MyClass();
// Mimic that variable being set by the page
mc.var = 2;
mc.aMethod();
System.assert(…); // Assert that the operations in the second branch worked
}
public static void testMethod testAMethod3(){
MyClass mc = new MyClass();
// Mimic that variable being set by the page
mc.var = 9;
mc.aMethod();
System.assert(…); // Assert that the operations in the last branch worked
}
[/code]
The for-Loop Congruence
Our code sample in this case might be,
[code language=”java”]
public class MyClass{
public List<Account> accounts{get;set;}
public void aMethod(){
// Debug should go here: System.debug(‘accounts:’+accounts);
for(Account a: accounts){
// Insert logic here
}
}
public void anotherMethod(){
for(Account a: [SELECT id FROM Account WHERE condition = true]){
// Insert logic here
}
}
}
[/code]
If you’re not getting code coverage in situations like those in aMethod() and anotherMethod(), you probably have an empty-list situation i.e. the list ‘accounts’ is empty, or the ‘SELECT id FROM Account WHERE condition = true’ query is not returning any values. A few well placed System.debug() messages will tell you where you’re going wrong.
The Exception Fluctuation
The last – and trickiest – case I’ll cover is exception handlers. The reason they’re more complex is that you not only have to test that your code works with expected input (and situations), but that it deals with errors gracefully (something we should all be doing anyway). Let’s have a look at an example situation,
[code language=”java”]
public class MyClass{
public String accountName{get;set;}
public void aMethod(){
try{
Account a = [SELECT id FROM Account WHERE name = :accountName];
} catch (System.queryException e){
// Deal with exception in logical and superfun way
}
}
}
[/code]
I would suggest writing a single test method to cover the try-part, and a separate test method to cover the catch-part,
[code language=”java”]
public static void init(){ // This is an aside. I recommend setting up your own test data so that any unit tests
// are independent of the Org that you have them in.
Account a = new Account(name=’A name that I know doesn\’t exist in my real dataset’);
insert a; // The data inserted will on exist (and be available) for the lifetime of the test method that executes it.
}
/** Positive Test **/
public static void testMethod testAMethod1(){
init();
MyClass mc = new MyClass();
mc.accountName = ‘A name that I know doesn\’t exist in my real dataset’;
mc.aMethod();
List<Account> a = [SELECT id FROM Account WHERE name = ‘A name that I know doesn\’t exist in my real dataset’];
System.assert(a.size()>0);
}
/** Negative Test **/
public static void testMethod testAMethod1(){
// No call to init() since we want an exception to be thrown
MyClass mc = new MyClass();
mc.accountName = ‘A name that I know doesn’t exist in my real dataset’;
mc.aMethod();
List<Account> a = [SELECT id FROM Account WHERE name = ‘A name that I know doesn\’t exist in my real dataset’];
System.assert(a.size()==0);
}
[/code]
Testing exceptions can be further complicated where you need to catch more than one type of exception,
[code language=”java”]
public void aMethod(){
try{
Account a = [SELECT id FROM Account WHERE name = :accountName];
a.name = ‘A new name’;
update a;
} catch (System.QueryException e){
// Deal with exception in logical and superfun way
} catch (System.DmlException e){
// Deal with this exception differently
}
}
[/code]
Here I’d suggest we have a test case for the try-part, as well as one for each of the catch-statements. This makes dev life a bucket-load more boring; take heart though, things could be worse, at least you’re not a banker.
These samples are relatively simple, but you’ll notice that even your most complex code is mostly an assortment of these situations. One feature of good software development is being able to break a complex problem down into tiny bits and analyse those independently, so get out your hard-hat and microscope, and let’s get our testing-thang on.
Great article Wes. I didn’t enjoy writing Apex unit tests until I learned the following:
* Always run tests from Eclipse
* Follow the Eclipse code coverage report for untested lines
* Create classes specifically for testing
* Use the @IsTest attribute on test classes and methods so the lines of code don’t count against org limit
-Mike
Very good points, thanks Mike. On your first point I’d even be so bold as to say, if you’re working in a team, and aren’t a n00b developer, all coding should be done from within Eclipse.
testing is way more fun if you do test-driven development. but yea, deep code branches can be a chore.
Indeed! Test-driven development FTW. For those who haven’t heard of TDD before wikipedia says, “Test-driven development (TDD) is a software development technique that relies on the repetition of a very short development cycle: First the developer writes a failing automated test case that defines a desired improvement or new function, then produces code to pass that test and finally refactors the new code to acceptable standards”.
Very nicely done, ole chap! I very much fancy your article. I think the big take away from your article for newcomers is “…would suggest a test method per value that βvarβ can assume…”. Like you said it can seem tedious but that’s the best way to test all variations without going crazy. Would love to see Salesforce use something like JUnit with setup and tear down methods. Thanks for the info!!
A couple of useful advices in here! Indeed cutting down your test classes into smaller parts will help you when you test class suddenly fail.
And I would like to emphasize the fact that Eclipse IDE Test plugin is not as good as it should be for professional development (no method selection, filter don’t work properly, the output window is one the worst, the double click on an error works once every 2 years etc….).
A quick look at the JUnit integration in Eclipse show us how it should be.
Ah, good ol’ JUnit.. man those were the days;)
Just getting to this article now, and it was a good read with some great takeaway information.
Writing test classes are always the worst part of my day, but yes, at least i am not a banker. π
This article was great. Saved my morning from being wasted banging my head against the wall while trying to figure out why my for and if statements didn’t have coverage.
It’s always great to know that I’ve been able to help π
Very helpful!
But I’m searching for an answer to another question:
Why are the test percentages in Eclipse different than the percentages that come up when running all tests in the Salesforce front end? For example, I have 3 classes with coverage over 75% when running tests in Eclipse, but they are considerably lower when running all tests (~50%).
Have you found this on any of your projects?
Thank you!
There’s a bug in the wild – I’ll post about it this week but for now just clear all test history. That’s probably it…