Join us as we look at unit tests in Apex - what they are and where they fit within an efficient and effective testing strategy. We'll also consider the demands that implementing such a strategy makes on how Apex code is structured in a Force.com application. You'll leave with an appreciation of the test pyramid, and some specific examples of mocking techniques.
TrustArc Webinar - Stay Ahead of US State Data Privacy Law Developments
Tests and Testability: Apex Structure and Strategy
1. Tests and Testability
Apex Structure and Strategy
Stephen Willcock, FinancialForce.com, Director of Product Innovation
@stephenwillcock
2. All about FinancialForce.com
Revolutionizing the Back Office
#1 Accounting, Billing and PSA Apps on the Salesforce platform
▪ Native apps
▪ San Francisco HQ, 595 Market St
▪ R&D in San Francisco, Harrogate UK, and Granada ES
▪ We are hiring! Meet us at Rehab!
3. Tests and Testability - overview
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
4. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
5. Testing strategy
In an ideal world we would test…
An entire system “end-to-end”
Using different types of user
With production data volumes
With complex / varied data profiles
All possible code paths
SIMULTANEOUSLY!
8. Test pyramid
The test pyramid is a concept
developed by Mike Cohn....
[the] essential point is that you
should have many more lowlevel unit tests than high level
end-to-end tests running
through a GUI.
http://martinfowler.com/bliki/TestPyramid.html
9. Test pyramid
Even with good practices on writing them, end-to-end tests are
more prone to non-determinism problems, which can undermine
trust in them.
In short, tests that run end-to-end through the UI are: brittle,
expensive to write, and time consuming to run.
So the pyramid argues that you should do much more
automated testing through unit tests than you should through
traditional GUI based testing.
http://martinfowler.com/bliki/TestPyramid.html
10. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
11. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
12. Unit test principles - what is a unit?
The smallest testable chunk of code
Independent from other units and systems
Uno
15. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
16. Unit test principles - isolation
https://
Uno
Uno
Uno
Uno
@
Uno
Trigger
Validation Rule
Workflow Rule
Uno
Managed Apex
Database
Related Data
…further
dependencies
18. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
19. Unit test principles - saturation
The Force.com platform requires that at least 75% of the Apex
Code in an org be executed via unit tests in order to deploy the
code to production. You shouldn’t consider 75% code coverage to
be an end-goal though
Instead, you should strive to increase the state coverage of
your unit tests
Code has many more possible states than it has lines of code
http://wiki.developerforce.com/page/How_to_Write_Good_Unit_Tests
21. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
22. Unit test principles - expectation
Unit test
•Stakeholder: developers
•Asks: does this code do what it says it will?
System test
•Stakeholder: Business Analyst
•Asks: does this system fulfil my functional requirements?
23. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
24. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
26. Unit test principles - testability
Well structured, Object Oriented code is likely to be testable:
•Encapsulation - well defined inputs and outputs
•Limited class scope
•Limited class size
•Limited method size
TDD
27. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
29. SObject fabrication - the unit
trigger OpportunityLineItems on OpportunityLineItem
(before insert) {
for(OpportunityLineItem item : Trigger.new) {
if(item.Description==null)
item.Description = 'foo';
}
}
30. SObject fabrication - the test
AccountId
StageName
OpportunityLineItem oli = new OpportunityLineItem (
insert new
CloseDate
Description=null );
Opportunity
insert oli;
insert new
oli = [select Description from OpportunityLineItem where
Account
OpportunityId
id=:oli.Id];
UnitPrice
Quantity
insert new
insert new
system.assertEquals('foo',oli.Description);
PricebookEntryId
PricebookEntry
Product2
Select Id from
Pricebook2
31. SObject fabrication - the test
a = new Account(…); insert a;
o = new Opportunity(…); insert o;
pb = [select Id from Pricebook2 … ];
p = new Product2(…); insert p;
pbe = new PricebookEntry(…); insert pbe;
oli = new OpportunityLineItem(…); insert oli;
oli = [select … from OpportunityLineItem …];
system.assertEquals('foo',oli.Description);
32. SObject fabrication - the revised unit
trigger OpportunityLineItems on OpportunityLineItem
(before insert) {
new OpportunityLineItemsTriggerHandler().beforeInsert(
Trigger.new );
}
Testable code:
break up the Trigger
33. SObject fabrication - the revised unit
public class OpportunityLineItemsTriggerHandler {
public void beforeInsert(List<OpportunityLineItem>
items) {
for(OpportunityLineItem item : items) {
if(item.Description==null)
item.Description = 'foo';
}
}
}
Avoid referring to
Trigger variables in the
handler
Testable code:
break up the Trigger
34. SObject fabrication - the revised test
OpportunityLineItem oli = new OpportunityLineItem(
Description=null );
new OpportunityLineItemsTriggerHandler().beforeInsert(
new List<OpportunityLineItem>{oli});
system.assertEquals('foo',oli.Description);
35. SObject fabrication #2 - the unit
public class OpportunityService {
public void adjust(OpportunityLineItem oli) {
oli.UnitPrice += (oli.UnitPrice *
oli.Opportunity.Account.Factor__c);
}
}
36. SObject fabrication #2 - the test
Account a = new Account(Factor__c=0.1);
Opportunity o = new Opportunity(Account=a);
OpportunityLineItem oli = new OpportunityLineItem(
Opportunity=o, UnitPrice=100);
OpportunityService svc = new OpportunityService();
svc.adjust(oli);
system.assertEquals(110,oli.UnitPrice);
37. SObject fabrication - what did we do?
Structured the code to make it easier to test
•Trigger handler / Trigger
Fabricated SObjects (including relationships)
•In-memory
•No database interaction
38. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
39. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
40. Loose type coupling
Use inheritance to “loosen” a relationship
•Interface
•Superclass
Substitute a mock “sibling implementation” during unit tests
42. Loose type coupling - the provider (test subject)
public class Aggregator {
List<OrderController.Item> items;
public void setItems(List<OrderController.Item> items) {
this.items = items; }
public Decimal getSum() {
Decimal result = 0;
for(OrderController.Item item : this.items)
result += item.getValue();
return result;
}
}
43. Loose type coupling - the provider test
List<OrderController.Item> testItems = new
List<OrderController.Item>{ new OrderController.Item(…), … };
Aggregator testAggregator = new Aggregator();
testAggregator.setItems(testItems);
system.assertEquals(123.456,testAggregator.getSum());
44. Loose type coupling - the revised provider
public class Aggregator {
public interface IItem {
Decimal getValue();
}
public void setItems(List<IItem> items) {…}
45. Loose type coupling - the revised provider
public Decimal getSum() {
Decimal result = 0;
for(IItem item : items)
result += item.getValue();
return result;
}
}
46. Loose type coupling - the revised consumer
public controller OrderController {
…
public class Item implements Aggregator.IItem {…}
Aggregator a…
List<Item> items…
a.setItems(items);
Decimal s = a.getSum();
47. Loose type coupling - the revised provider test
class TItem implements Aggregator.IItem {
Decimal value;
TItem(Decimal d) {
value = d;
}
public getValue() {
return value;
}
}
48. Loose type coupling - the revised provider test
List<TItem> testItems = new List<TItem>{ new TItem(100),
new TItem(20.006), new TItem(3.45) };
Aggregator testAggregator = new Aggregator();
testAggregator.setItems(testItems);
system.assertEquals(123.456,testAggregator.getSum());
49. Loose type coupling - what did we do?
OrderController.Item
Aggregator.IItem
TItem
Aggregator
Production
Unit
Test
50. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
51. Dependency Injection
Dependency Injection is all about injecting dependencies, or
simply said, setting relations between instances
Some people refer to it as being the Hollywood principle "Don't
call me, we'll call you”
I prefer calling it the "bugger" principle: "I don't care who
you are, just do what I ask”
http://www.javaranch.com/journal/200709/dependency-injection-unit-testing.
html
55. Dependency injection - provider
public with sharing class OpportunityAdjuster implements
IAdjustOpportunities {
public void adjust(Opportunity o) {
// the actual implementation
// do some stuff to the opp
}
}
56. Dependency injection - mock provider
public with sharing class TOpportunityAdjuster
implements IAdjustOpportunities {
@testVisible Opportunity opp;
@testVisible Boolean calledAdjust;
public void adjust(Opportunity o) {
opp = o;
calledAdjust = true;
}
}
@testVisible
57. Dependency injection - consumer
public class OpportunityController {
IAdjustOpportunities adjuster;
@testVisible OpportunityController(
IAdjustOpportunities a,
ApexPages.StandardController c ) {
this.adjuster = a;
…
}
58. Dependency injection - consumer
public OpportunityController(
ApexPages.StandardController c) {
this(new OpportunityAdjuster(), c);
}
…
public void makeAdjustment() {
adjuster.adjust(opp);
}
59. Dependency injection - consumer test
Opportunity opp = new Opportunity(…);
ApexPages.StandardController sc = new
ApexPages.StandardController(opp);
TOpportunityAdjuster adjuster = new TOpportunityAdjuster();
OpportunityController oc = new
OpportunityController(adjuster, sc);
oc.makeAdjustment();
system.assert(adjuster.calledAdjust);
system.assertEquals(opp,adjuster.opp);
Constructor
injection
60. Dependency injection - what did we do?
• Loosen the coupling to a provider class in a consumer class
• Mock the provider class
• Inject the mock provider implementation into the consumer via
a new @testVisible constructor on the consumer class to test
the consumer class
61. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
62. SObject Decoupler - the unit
public class OpportunitiesTriggerHandler {
public static void afterUpdate(List<Opportunity> items) {
for(Opportunity item : items) {
if(item.IsClosed){
// do something
}
}
}…
}
63. SObject Decoupler - the test
@isTest private class OpportunitiesTriggerHandlerTest {
@isTest static void myTest() {
Opportunity o = new Opportunity(IsClosed=true);
OpportunitiesTriggerHandler handler = new
OpportunitiesTriggerHandler();
Field is not writeable:
handler.afterUpdate(new List<Opportunity>{o});
Opportunity.IsClosed
// test something
}
SObject fabrication limitations:
formula fields, rollup summaries,
system fields, subselects
64. SObject Decoupler - the decoupler
public virtual class OpportunityDecoupler {
public virtual Boolean getIsClosed( Opportunity o ) {
return o.IsClosed;
}
}
65. SObject Decoupler - the test decoupler
public virtual class TOpportunityDecoupler extends
OpportunityDecoupler {
@testVisible Map<Id,Boolean> IsClosedMap =
new Map<Id,Boolean>();
public override Boolean getIsClosed( Opportunity o ) {
return IsClosedMap.get(o.Id);
}
}
66. SObject Decoupler - the revised unit
public class OpportunitiesTriggerHandler {
OpportunityDecoupler decoupler;
@testVisible OpportunitiesTriggerHandler(
OpportunityDecoupler od ) {
this.decoupler = od;
}
public OpportunitiesTriggerHandler() {
this(new OpportunityDecoupler());
}
Constructor
injection
67. SObject Decoupler - the revised unit
public void afterUpdate(List<Opportunity> items) {
for(Opportunity item : items) {
if(decoupler.getIsClosed(item)) {
// do something
}
}
}
}
68. SObject Decoupler - the revised test
@isTest private class OpportunitiesTriggerHandlerTest {
@isTest static void myTest() {
TOpportunityDecoupler decoupler = new
TOpportunityDecoupler();
Opportunity o = new Opportunity(Id =
TestUtility.getFakeId(Opportunity.SObjectType));
decoupler.IsClosedMap.put(o.Id,true);
Fabrication of
SObject IDs
69. SObject Decoupler - the revised test
public with sharing class TestUtility {
static Integer s_num = 1;
}
public static String getFakeId(Schema.SObjectType sot) {
String result = String.valueOf(s_num++);
return sot.getDescribe().getKeyPrefix() +
'0'.repeat(12-result.length()) + result;
}
Fabrication of
SObject IDs
70. SObject Decoupler - the revised test
OpportunitiesTriggerHandler handler = new
OpportunitiesTriggerHandler(decoupler);
handler.afterUpdate(new List<Opportunity>{o});
// test something
}
71. SObject Decoupler - what did we do?
Mechanism for mocking non-writable SObject properties
•Access the SObject properties via a separate virtual class - the
decoupler
•Decoupler subclass mocks access to non-writable SObject
properties
•Inject the decoupler subclass in the test subject constructor
73. Tests and Testability
Testing strategy
• Test pyramid
Unit test principles
• What is a unit?
• Isolation
• Saturation
• Expectation
Unit test techniques
• Testability
77. Adapter - wrapper
public class LockingRuleHandler implements
IHandleLockingRules {
public void handleAfterUpdate(Map<Id,sObject> oldMap,
Map<Id,sObject> newMap) {
LockingRules.LockingRuleHandler.handleTrigger();
}
}
78. Adapter - mock implementation
public class TLockingRuleHandler implements
IHandleLockingRules {
@testVisible Boolean calledHandleAfterUpdate;
public void handleAfterUpdate(Map<Id,sObject> oldMap,
Map<Id,sObject> newMap) {
calledHandleAfterUpdate = true;
}
}
79. Adapter - the unit
public class OpportunitiesTriggerHandler {
IHandleLockingRules lockingRules;
@testVisible OpportunitiesTriggerHandler(
IHandleLockingRules lr ) {
this.lockingRules = lr;
}
public OpportunitiesTriggerHandler() {
this(new LockingRuleHandler());
}
Constructor
injection
80. Adapter - the unit
public void handleAfterUpdate(Map<Id,sObject> oldMap,
Map<Id,sObject> newMap) {
lockingRules.handleAfterUpdate(oldMap, newMap);
}
81. Adapter - the test
Map<Id,Opportunity> oldMap …
Map<Id,Opportunity> newMap …
TLockingRuleHandler lockingRules = new
TLockingRuleHandler();
OpportunitiesTriggerHandler trig = new
OpportunitiesTriggerHandler(lockingRules);
trig.afterUpdate(oldMap,newMap);
system.assert(lockingRules.calledHandleAfterUpdate);
…
82. Adapter - what did we do?
Mock a managed class
•Create an interface defining our expectations of the managed
class
•Adapt the the managed class by wrapping and implementing
the interface
•Mock the production class by implementing the same interface
•Inject the mock implementation during unit test execution
83. In a nutshell…
Unit tests are foundational to an effective Apex testing strategy
Consider testability in the structure / design of your code
Units must be independent to be easily tested
Units can be made independent through fabrication and
substitution of connected resources