What is Unit Test Coverage?
Use the isTest annotation to define classes or individual methods that only
contain code used for testing your application. The isTest annotation is similar to creating methods declared as testMethod. Classes defined with the isTest annotation don't count against your organization limit of 3MB for all Apex code. Individual methods defined with
the isTest annotation do count against your organization limits
starting with Apex code
saved using Salesforce.com API version 24.0, test methods don’t have access by default to
pre-existing data in the organization. However, test code saved against Salesforce.com API version
23.0 or earlier continues to have access to all data in the organization and
its data access is unchanged. Classes
and methods defined as isTest can be either private or public. Classes
defined as isTest must be
top-level classes.
This is an example of a private test class that
contains two test methods.
@isTest
private class MyTestClass {
// Methods for testing
@isTest static void test1() {
// Implement test code
}
@isTest static void test2() {
// Implement test code
}
}
This is an example
of a public test class that contains a utility method for test data creation:
@isTest
public class TestUtil {
public static void createTestData() {
// Create some test invoices
}
}
Classes defined as isTest can't be interfaces or enums.
Methods
of a public test class can only be called from a running test, that is, a test
method or code invoked by a test method, and can't be called by a non-test
request
Example:
the HelloWorld Class shows an
Apex function that tests whether the text of a string matches "Hello
World" or not.
// HelloWord Class
public class HelloWorld {
public static String isHelloWorld(String myString) {
if (myString.equals('Hello World')) return 'Yes it is!'; //
Line 1
else return 'No it is not!': // Line 2
}
}
The verifyIsHelloWorld test method exercises our
gratuitous example
// verifyIsHelloWorld test method
@isTest
private class TEST_HelloWorld {
static testMethod void verifyIsHelloWorld () {
String outcome = HelloWorld.isHelloWorld ('Hello World');
System.assert(outcome == 'Yes it is!', 'Expected positive
message.') ;
}
}
At this point, we have 66% test coverage, because our test
exercises only one of the two statements in isHelloWorld (line 1).
To bring test coverage up to 100%, we need to add another
test method.
// Another test method
static testMethod void verifyIsHelloWorldFalse() {
String outcome = HelloWorld.isHelloWorld ('Hello Kitty');
System.assert(outcome == 'No it is not!', 'Expected negative message.')
;
}
With both of these test methods in play, our code now has
100% coverage.
Why is test coverage important?
Salesforce.com has high standards for its own code and expects
custom Apex code to also be robust and error-free. One of the best ways to
increase code quality is to encourage developers to write unit tests.
While it's possible for developers to
boost code coverage with pointless tests, hardcore coders see unit tests as a
way to release better code sooner -- the keyword being "release". The
time spent on proactive unit testing is a trade-off with the time spent on
reactive debugging. We can find our own bugs ourselves with unit tests, or wait
and fix them later when a feature comes back with a QA ticket attached.
In my own work, I've found that
the best way to ensure that code has adequate test coverage to practice
test-driven development (TDD).
What is Test Driven Development
(TDD)?
For the uninitiated, a classic
way to bootstrap unit testing (and TDD) is to start with defect reports. Before
fixing a bug, a developer first writes a test that proves that the defect
exists. For example, if someone reports that isHelloWorld fails if we pass in a
null string, we could start with a test like the one shown by
verifyIsHelloWorldNull.
// verifyIsHelloWorldNull
static testMethod void verifyIsHelloWorldNull() {
String outcome = HelloWorld.isHelloWorld (null);
System.assert(outcome == 'No it is not!', 'Expected negative message.')
;
}
If we run this test, it raises an exception
“System.NullPointerException: Attempt to de-reference a null object”.
Since an exception counts as a failing test, we can
proceed with the fix, say, by changing the code from:
| if (myString.equals('Hello World')) return 'Yes it is!'; // Line
1
to:
| if 'HelloWorld'.equals('myString') return 'Yes it is!'; // Line
1
and maybe, for good measure, including a fourth test case
for an empty string.
// Test for empty String
final static String EMPTY = '';
static testMethod void verifyIsHelloWorldEmpty() {
String outcome = HelloWorld.isHelloWorld (EMPTY);
System.assert(outcome == 'No it is not!', 'Expected negative message.')
;
}
Once all of our tests are passing, we
could even refactor the code, and improve the internal design by using a
constant and a single comparison.
// Code refactored
public class HelloWorld {
public final static String HELLO_WORLD = 'Hello World';
public final static String YES_WORLD = 'Yes it is!';
public final static String NO_WORLD = 'No it is not!';
public static String isHelloWorld(String myString) {
return (HELLO_WORLD.equals(myString))
? YES_WORLD : NO_WORLD;
}
}
If our tests pass (they do), we can
be confident that our refactoring did not break the code's external behaviour.
Passing tests give us the courage to refine existing code and improve the
internal design. The key idea behind TDD is to "never write a line of code
without a failing test". If we are going to write the test anyway, better
to write it first, code to the test, and receive full benefit for the time we
invest.
How do we test code that
doesn't exist?
In an Apex environment, a unit
test usually operates at the class level. To bootstrap testing a class or
method that does not exist, we can start coding the test, create a stub class
with stub methods, sufficient to compile the test, confirm that it fails, and
then fill-in functionality to pass the test.
Let’s start over from scratch. First, we should define our
requirements for the isHelloWorld method.
//
isHelloWorld requirements
/**
* The isHelloWorld method determines if a String equals 'Hello World'.
* (1) Given the String 'Hello World', the method returns "Yes, it
Is."
* (2) Given some other String, the method returns 'No, it is not!'.
* (3) Given a null or empty string, the method returns 'No, it is not!'.
*/
Then, we can write a "happy path" test for the
first requirement.
// A happy path test for requirement (1)
final static String POSITIVE =
'Expected positive message.';
static testMethod void verifyIsHelloWorld () {
String outcome = HelloWorld.isHelloWorld ('Hello World');
System.assert(outcome == HelloWorld.YES_WORLD,POSITIVE ) ;
}
provide a stub Hello World class to compile the test
// A HelloWorld stub class
public class HelloWorld {
public static String isHelloWorld(String myString) {
return null;
}
}
add just enough behavior to pass one test for one
requirement
// Coding requirement (1)
public final static String
HELLO_WORLD = 'Hello World';
public final static String YES_WORLD = 'Yes it is!';
public static String isHelloWorld(String myString) {
return HELLO_WORLD.equals(myString) ? YES_WORLD : return null;
}
then another requirement
// Testing requirement (2)
final static String NEGATIVE
=’Expected negative message.’;
static testMethod void verifyIsHelloWorldFalse () {
String outcome = HelloWorld.isHelloWorld ('Hello Kitty');
System.assert(outcome == HelloWorld.NO_WORLD,NEGATIVE ) ;
}
// Coding requirements (1) and
(2)
public final static
String HELLO_WORLD = 'Hello World';
public final static String YES_WORLD = 'Yes it is!';
public final static String NO_WORLD = 'No it is not!';
public static String isHelloWorld(String myString) {
return HELLO_WORLD.equals(myString) ? YES_WORLD : NO_WORLD;
}
and a third
// Testing requirement (3)
static testMethod void
verifyIsHelloWorldNull() {
String outcome = HelloWorld.isHelloWorld (null);
System.assert(outcome == HelloWorld.NO_WORLD,NEGATIVE );
}
final static String EMPTY = '';
static testMethod void verifyIsHelloWorldEmpty() {
String outcome = HelloWorld.isHelloWorld (EMPTY);
System.assert(outcome == HelloWorld.NO_WORLD,NEGATIVE );
}
For requirement 3, we added two test methods, but did not need to change any
code, since the current implementation passed the tests.
To fully test the method, we might also add a test
for a string of maximum length, so that we test both boundaries. But, as it
stands, we have 100% test coverage, and a test for each stated requirement,
which meets my own personal "definition of done".
Three takeaways from this exercise are:
·
Never write a line of code without a failing test.
·
Test every requirement, one requirement at a time.
·
Passing tests give us the courage to refactor.
isTest(SeeAllData=true) Annotation
For Apex code saved
using Salesforce.com API version 24.0 and later, use theisTest(SeeAllData=true) annotation to grant test
classes and individual test methods access to all data in the organization,
including pre-existing data that the test didn’t create. Starting
with Apex code saved using Salesforce.com API version
24.0, test methods don’t have access by default to pre-existing data in the
organization. However, test code saved
against Salesforce.com API version 23.0 or earlier continues to
have access to all data in the organization and its data access is unchanged.
Considerations of the IsTest(SeeAllData=true) Annotation
- If a test class is defined with the isTest(SeeAllData=true) annotation, this annotation applies to
all its test methods whether the test methods are defined with the @isTest annotation or thetestmethod keyword.
- The isTest(SeeAllData=true) annotation is used to open up data
access when applied at the class or method level. However, using isTest(SeeAllData=false) on a method doesn’t restrict
organization data access for that method if the containing class has already
been defined with the isTest(SeeAllData=true) annotation. In this case, the method
will still have access to all the data in the organization.
This example shows how to define a test class with
the isTest(SeeAllData=true) annotation. All the test methods
in this class have access to all data in the organization.
//
All test methods in this class can access all data.
@isTest(SeeAllData=true)
public
class TestDataAccessClass {
// This test accesses an existing
merchandise item.
// It also creates and accesses a new test
merchandise item.
static testmethod void myTestMethod1() {
// Query an existing merchandise item
in the organization.
Merchandise__c m = [SELECT Id,
Price__c, Total_Inventory__c, Description__c
FROM Merchandise__c WHERE
Name='Pencils' LIMIT 1];
System.assert(m != null);
// Create a test merchandise item based
on the queried merchandise item.
Merchandise__c testMerchandise =
m.clone();
testMerchandise.Name = 'Test Pencil';
insert testMerchandise;
// Query the test merchandise that was
inserted.
Merchandise__c testMerchandise2 =
[SELECT Id, Price__c, Total_Inventory__c
FROM Merchandise__c
WHERE Name='Test Pencil' LIMIT 1];
System.assert(testMerchandise2 !=
null);
}
// Like the previous method, this test
method can also access all data
// because the containing class is
annotated with @isTest(SeeAllData=true).
@isTest static void myTestMethod2() {
// Can access all data in the
organization.
}
}
This
second example shows how to apply the isTest(SeeAllData=true) annotation on a
test method. Because the class that the test method is contained in isn’t
defined with this annotation, you have to apply this annotation on the test
method to enable access to all data for that test method. The second test
method doesn’t have this annotation, so it can access only the data it creates
in addition to objects that are used to manage your organization, such as
users.
//
This class contains test methods with different data access levels.
@isTest
private
class ClassWithDifferentDataAccess {
// Test method that has access to all data.
@isTest(SeeAllData=true)
static void testWithAllDataAccess() {
// Can query all data in the
organization.
}
// Test method that has access to only the
data it creates
// and organization setup and metadata
objects.
@isTest static void testWithOwnDataAccess()
{
// This method can still access the
User object.
// This query returns the first user
object.
User u = [SELECT UserName,Email FROM
User LIMIT 1];
System.debug('UserName: ' +
u.UserName);
System.debug('Email: ' + u.Email);
// Can access the test invoice that is
created here.
Invoice_Statement__c inv = new
Invoice_Statement__c(
Description__c='Invoice 1');
insert inv;
// Access the invoice that was just
created.
Invoice_Statement__c insertedInv =
[SELECT Id,Description__C
FROM
Invoice_Statement__c
WHERE
Description__c='Invoice 1'];
System.assert(insertedInv != null);
}
}