Apex Testing for Humans
and dumb robots
Who I am
Patrick Connelly
@aelst
- Salesforce Developer
- Force.com MVP
Why testing?
Because you have to, and you should anyway!
- 75% code coverage
- Better user experience
- Catch problems early
3 Testing Rules
Three main rules for testing
Rule 1
Test.startTest()
— Test.stopTest()
Code run between these two causes all limits to be reset. Should be used to make sure your code doesn't hit limits. Test setup can interfere with those limits.
Rule 2
Generate testing data in the test
Rule 3
Tests only evaluate a single part of functionality
Testing No-Nos
Things you should never do:
- no-op loops to increase code coverage
- Tests with no asserts
- Require real-world data*
No-Op AKA Null Operations
We get it. Testing somethings can be hard. The test code below would only return a code coverage of 66%
public static myMethod() {
if (reallyDifficultToReproduceBoolean) {
doSomething();
}
doSomethingElse();
}
static testMethod void myMethod_test() {
myMethod();
}
No-Op AKA Null Operations
The code below will increase your coverage to 83% but it will also kill at least 9 kittens.
DON'T EVER DO THIS! THINK OF THE KITTENS!!
public static myMethod() {
Integer i = 0;
if (i = 0) {
i = i;
}
if (reallyDifficultToReproduceBoolean) {
doSomething();
}
doSomethingElse();
}
static testMethod void myMethod_test() {
myMethod();
}
TestUtils
What do you mean TestUtils? Why?
- Reusability
- Change management
- You wouldn't rely on real data would you? Of course not
TestUtils — getters & setters
Let TestUtils generate your objects for you
//Keeps us from having duplicate names
static private NAME_COUNT = 10000;
static private NAME_INC = 1;
//Lets us set the name manually if we want
public static MyObject__c getMyObject(Account a, String name) {
return new MyObject__c(
Account__c = a.Id,
Name = name,
...
);
}
//Gives us an easy way to get a unique object.
// This will make bulk operations easier
public static MyObject__c getMyObject(Account a) {
MyObject__c result = getMyObject(a, '_unittest_name_: ' + NAME_COUNT);
NAME_COUNT += NAME_INC;
return result;
}
TestUtils — fetch
Let TestUtils fetch the stored object for you
public static MyObject__c fetchMyObject(MyObject__c obj) {
return [
select Account__c,
Name,
...
from MyObject__c
where Id = :obj.Id
];
}
public static List<MyObject__c> fetchMyObjects(Account a) {
return [
select Account__c,
Name,
...
from MyObject__c
where Account__c = :a.Id
];
}
Exceptions
When doing samurai programming,* you rely heavily on exceptions
Exceptions in SalesForce can kinda be a pain to test:
- Ensuring that an exception occurred
- Ensuring the right exception occurred
- Testing un-handled/unknown exceptions
Exceptions — The known exception
When you have a known path to generate an exception
public class MyClass {
public static String RESULT_STRING = 'Result data: ';
public static String NONZERO_MSG = 'Parameter must be non-zero';
public class BadParameterException extends Exception {}
public static String myMethod(Integer i) {
if (i == 0) {
throw new BadParameterException(NONZERO_MSG);
}
return RESULT_STRING + i;
}
}
Exceptions — Known exception testing
static testMethod void positiveTest() {
Integer i = 10;
String result = MyClass.myMethod(i);
String expectedResult = MyClass.RESULT_STRING + i;
System.assertEquals(expectedResult, result, 'Wrong result');
}
static testMethod void negativeTest() {
Integer i = 0;
try {
String result = MyClass.myMethod(i);
System.assert(false, 'We should have thrown an exception');
} catch (MyClass.BadParameterException e) {
System.assertEquals(
MyClass.NONZERO_MSG, e.getMessage(),
'Right exception type, wrong message'
);
}
}
Exceptions — Unknown exceptions
When you could have an exception but can't reproduce it
global with sharing class MyAPI {
public static String ERR_MSG = 'OH NOES!';
WebService static String myWebservice(Integer i) {
String result = i;
try {
MyOtherClass.somePotentiallyDangerousMethod(i);
} catch (Exception e) {
result = ERR_MSG;
}
return result;
}
}
global with sharing class MyAPI {
public static Boolean THROW_EXCEPTION = false;
public class UnhandledException extends Exception {}
public static void generateExceptionForTesting() {
if (Test.isRunningTest() && THROW_EXCEPTION) {
throw new UnhandledException('Exception for testing');
}
}
public static String ERR_MSG = 'OH NOES!';
WebService static String myWebservice(Integer i) {
String result = i;
try {
generateExceptionForTesting();
MyOtherClass.somePotentiallyDangerousMethod(i);
} catch (Exception e) {
result = ERR_MSG;
}
return result;
}
}
Exceptions — Unknown exceptions testing
static testMethod void negativeTest() {
Integer i = 0;
Test.startTest();
MyAPI.THROW_EXCEPTION = true;
String result = MyAPI.myWebservice(i);
Test.stopTest();
System.assertEquals(MyAPI.ERR_MSG, result, 'Wrong result');
}
Bulk data operations
When writing tests, don't forget to test in bulk
You may think that you will never do anyting in bulk with your data, but you might, and you'll spend lots of time debugging and cursing.
Things to consider when making your triggers to make them bulk ready:
- Don't assume only one record:
trigger.new().get(0)
is bad - No DML in loops
- Insert / update lists
- Build up ids/where items for SOQL
TestUtils & bulk data
Adding bulk data operations to your TestUtils class will allow you to easily create large sets of data
public static Integer DEFAULT_BULK_COUNT = 200;
List<MyObject__c> getMyObjects(Integer count) {
List<MyObject__c> result = new List<MyObject__c>();
for (Integer i = 0; i < count; i++) {
result.add(getMyObject());
}
return result;
}
List<MyObject__c> getMyObjects() {
return getMyObjects(DEFAULT_BULK_COUNT);
}
Bulk data testing
By defining our expected results we can ensure we get back all the ones we expect and no results we don't
Set<Id> expectedResults = new Set<Id> {
testObj1.Id, testObj2.Id, testObj3.Id
};
Test.startTest();
List<MyObject__c> results = MyClass.someMethod();
Test.stopTest();
for (MyObject__c obj: results) {
if (!expectedResults.contains(obj.Id)) {
System.assertEquals(false, 'Got an object we did not expect');
}
expectedResults.remove(obj.Id);
}
System.assertEquals(expectedResults.isEmpty(),
'Did not get back all results [' + expectedResults + ']');
Test Visible
Introduced Winter '14 — Allows tests to directly access private
variables and methods in your test
public class MyClass {
@testVisible private String data;
public MyClass() {}
public setString(String s) {
this.data = s;
}
}
static testMethod void myTest() {
String testData = '_unittest_string_: 001';
MyClass mc = new MyClass();
mc.setString(testData);
System.assertEquals(testData, mc.data, 'Wrong data');
}
Questions?
/
#