Tests and TestabilityApex Structure and Strategy
Stephen Willcock, FinancialForce.com, Director of Product Innovation@stephenwillcock
All about FinancialForce.comRevolutionizing 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!
Tests and Testability - overviewTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Testing strategyIn an ideal world we would test…An entire system “end-to-end”Using different types of user With production data volumesWith complex / varied data profilesAll possible code paths
SIMULTANEOUSLY!
Test pyramid
More numerous
tests
Fewer tests More complex
tests
Less complex
tests
Test pyramid
Test pyramidThe test pyramid is a concept developed by Mike Cohn.... [the] essential point is that you should have many more low-level unit tests than high level end-to-end tests running through a GUI.
http://martinfowler.com/bliki/TestPyramid.html
Test pyramidEven 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
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Unit test principles - what is a unit?The smallest testable chunk of codeIndependent from other units and systems
Uno
Unit test principles - what is a unit?
Unit test principles - what is a unit?
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Unit test principles - isolation
Uno Uno
Database
https://
@Uno
UnoTriggerUno
Related Data
Uno
…further dependencies
Managed Apex
Workflow Rule
Validation Rule
Unit test principles - isolation
Uno Uno
Database
Mocked resources
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Unit test principles - saturationThe 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 thoughInstead, you should strive to increase the state coverage of your unit testsCode has many more possible states than it has lines of code
http://wiki.developerforce.com/page/How_to_Write_Good_Unit_Tests
Unit test principles - saturation
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Unit test principles - expectationUnit 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?
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Unit test principles - testability
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 sizeTDD
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Evaluate and assert Read data Database commit
Triggered record update
Unit test
Trigger firesInsert test data
SObject fabrication
Insert supporting data
SObject fabrication - the unittrigger OpportunityLineItems on OpportunityLineItem
(before insert) {
for(OpportunityLineItem item : Trigger.new) {
if(item.Description==null)
item.Description = 'foo';
}
}
SObject fabrication - the testOpportunityLineItem oli = new OpportunityLineItem (
Description=null );
insert oli;
oli = [select Description from OpportunityLineItem where
id=:oli.Id];
system.assertEquals('foo',oli.Description);
OpportunityId
UnitPrice
Quantity
PricebookEntryId
insert new
PricebookEntry
insert new
Opportunity
insert new
Product2
Select Id from
Pricebook2
AccountId
StageName
CloseDate
insert new
Account
SObject fabrication - the testa = 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);
SObject fabrication - the revised unittrigger OpportunityLineItems on OpportunityLineItem
(before insert) {
new OpportunityLineItemsTriggerHandler().beforeInsert(
Trigger.new );
}
Testable code: break up the Trigger
SObject fabrication - the revised unitpublic class OpportunityLineItemsTriggerHandler {
public void beforeInsert(List<OpportunityLineItem>
items) {
for(OpportunityLineItem item : items) {
if(item.Description==null)
item.Description = 'foo';
}
}
} Testable code: break up the Trigger
Avoid referring to Trigger variables in the
handler
SObject fabrication - the revised testOpportunityLineItem oli = new OpportunityLineItem(
Description=null );
new OpportunityLineItemsTriggerHandler().beforeInsert(
new List<OpportunityLineItem>{oli});
system.assertEquals('foo',oli.Description);
SObject fabrication #2 - the unitpublic class OpportunityService {
public void adjust(OpportunityLineItem oli) {
oli.UnitPrice += (oli.UnitPrice *
oli.Opportunity.Account.Factor__c);
}
}
SObject fabrication #2 - the testAccount 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);
SObject fabrication - what did we do?Structured the code to make it easier to test•Trigger handler / TriggerFabricated SObjects (including relationships)•In-memory•No database interaction
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Loose type couplingUse inheritance to “loosen” a relationship•Interface•SuperclassSubstitute a mock “sibling implementation” during unit tests
Loose type coupling
OrderController.Item
Aggregator
OrderController(Custom
Controller)
ProviderConsumer
Unit test
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;
}
}
Loose type coupling - the provider testList<OrderController.Item> testItems = new
List<OrderController.Item>{ new OrderController.Item(…), … };
Aggregator testAggregator = new Aggregator();
testAggregator.setItems(testItems);
system.assertEquals(123.456,testAggregator.getSum());
Loose type coupling - the revised providerpublic class Aggregator {
public interface IItem {
Decimal getValue();
}
public void setItems(List<IItem> items) {…}
Loose type coupling - the revised provider public Decimal getSum() {
Decimal result = 0;
for(IItem item : items)
result += item.getValue();
return result;
}
}
Loose type coupling - the revised consumerpublic controller OrderController {
…
public class Item implements Aggregator.IItem {…}
Aggregator a…
List<Item> items…
a.setItems(items);
Decimal s = a.getSum();
Loose type coupling - the revised provider testclass TItem implements Aggregator.IItem {
Decimal value;
TItem(Decimal d) {
value = d;
}
public getValue() {
return value;
}
}
Loose type coupling - the revised provider testList<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());
Loose type coupling - what did we do?
Production Unit Test
Aggregator.IItemOrderController.Item
Aggregator
TItem
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Dependency InjectionDependency Injection is all about injecting dependencies, or simply said, setting relations between instancesSome 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
Dependency injection
OpportunityAdjuster
OpportunityController
Opportunity o;
OpportunityAdjuster a;
a = new OpportunityAdjuster();
a.adjust(o);
ProviderConsumer
Unit test
Dependency injection
OpportunityController
TOpportunityAdjuster
OpportunityController
OpportunityAdjuster
Inject dependency
Production usage
Unit test usage
Dependency injection - interfacepublic interface IAdjustOpportunities {
void adjust(Opportunity o);
}
Dependency injection - providerpublic with sharing class OpportunityAdjuster implements
IAdjustOpportunities {
public void adjust(Opportunity o) {
// the actual implementation
// do some stuff to the opp
}
}
Dependency injection - mock providerpublic with sharing class TOpportunityAdjuster
implements IAdjustOpportunities {
@testVisible Opportunity opp;
@testVisible Boolean calledAdjust;
public void adjust(Opportunity o) {
opp = o;
calledAdjust = true;
}
}
@testVisible
Dependency injection - consumerpublic class OpportunityController {
IAdjustOpportunities adjuster;
@testVisible OpportunityController(
IAdjustOpportunities a,
ApexPages.StandardController c ) {
this.adjuster = a;
…
}
Dependency injection - consumer public OpportunityController(
ApexPages.StandardController c) {
this(new OpportunityAdjuster(), c);
}
…
public void makeAdjustment() {
adjuster.adjust(opp);
}
Dependency injection - consumer testOpportunity 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
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
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
SObject Decoupler - the unitpublic class OpportunitiesTriggerHandler {
public static void afterUpdate(List<Opportunity> items) {
for(Opportunity item : items) {
if(item.IsClosed){
// do something
}
}
} …
}
SObject Decoupler - the test@isTest private class OpportunitiesTriggerHandlerTest {
@isTest static void myTest() {
Opportunity o = new Opportunity(IsClosed=true);
OpportunitiesTriggerHandler handler = new
OpportunitiesTriggerHandler();
handler.afterUpdate(new List<Opportunity>{o});
// test something
}
Field is not writeable:
Opportunity.IsClosed
SObject fabrication limitations: formula fields, rollup summaries,
system fields, subselects
SObject Decoupler - the decouplerpublic virtual class OpportunityDecoupler {
public virtual Boolean getIsClosed( Opportunity o ) {
return o.IsClosed;
}
}
SObject Decoupler - the test decouplerpublic 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);
}
}
SObject Decoupler - the revised unitpublic class OpportunitiesTriggerHandler {
OpportunityDecoupler decoupler;
@testVisible OpportunitiesTriggerHandler(
OpportunityDecoupler od ) {
this.decoupler = od;
}
public OpportunitiesTriggerHandler() {
this(new OpportunityDecoupler());
}
Constructor injection
SObject Decoupler - the revised unit public void afterUpdate(List<Opportunity> items) {
for(Opportunity item : items) {
if(decoupler.getIsClosed(item)) {
// do something
}
}
}
}
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
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
SObject Decoupler - the revised test OpportunitiesTriggerHandler handler = new
OpportunitiesTriggerHandler(decoupler);
handler.afterUpdate(new List<Opportunity>{o});
// test something
}
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
SObject Decoupler - useful for…Mocking:•Formula fields•System fields•Rollup summary fields•Subselects
• Select (Select ... From OpportunityLineItems) From Opportunity
Tests and TestabilityTesting strategy• Test pyramidUnit test principles• What is a unit?• Isolation• Saturation• ExpectationUnit test techniques• Testability• Fabrication• Substitution
• Loose type coupling• Dependency Injection• Decoupler• Adapter
Adapter - for managed classes
LockingRules.LockingRule
Handler(managed class)
IHandleLockingRules
OpportunitiesTrigger Handler
LockingRuleHandler
(wrapper)
Test LockingRuleHandler
implements IHandleLockingRules
Adapter - managed class
global class
LockingRuleHandler
static void handleTrigger()
Adapter - interfacepublic interface IHandleLockingRules {
void handleAfterUpdate(Map<Id,sObject> oldMap,
Map<Id,sObject> newMap);
}
Adapter - wrapperpublic class LockingRuleHandler implements
IHandleLockingRules {
public void handleAfterUpdate(Map<Id,sObject> oldMap,
Map<Id,sObject> newMap) {
LockingRules.LockingRuleHandler.handleTrigger();
}
}
Adapter - mock implementationpublic class TLockingRuleHandler implements
IHandleLockingRules {
@testVisible Boolean calledHandleAfterUpdate;
public void handleAfterUpdate(Map<Id,sObject> oldMap,
Map<Id,sObject> newMap) {
calledHandleAfterUpdate = true;
}
}
Adapter - the unitpublic class OpportunitiesTriggerHandler {
IHandleLockingRules lockingRules;
@testVisible OpportunitiesTriggerHandler(
IHandleLockingRules lr ) {
this.lockingRules = lr;
}
public OpportunitiesTriggerHandler() {
this(new LockingRuleHandler());
}
Constructor injection
Adapter - the unitpublic void handleAfterUpdate(Map<Id,sObject> oldMap,
Map<Id,sObject> newMap) {
lockingRules.handleAfterUpdate(oldMap, newMap);
}
Adapter - the testMap<Id,Opportunity> oldMap …
Map<Id,Opportunity> newMap …
TLockingRuleHandler lockingRules = new
TLockingRuleHandler();
OpportunitiesTriggerHandler trig = new
OpportunitiesTriggerHandler(lockingRules);
trig.afterUpdate(oldMap,newMap);
system.assert(lockingRules.calledHandleAfterUpdate);
…
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
In a nutshell…Unit tests are foundational to an effective Apex testing strategyConsider testability in the structure / design of your codeUnits must be independent to be easily testedUnits can be made independent through fabrication and substitution of connected resources
Going forward…Tests and Testability on foobarforce.comSample code on Github@stephenwillcock
Stephen Willcock
Director of Product Innovation at FinancialForce.
com@stephenwillcock
Top Related