tests and testability: apex structure and strategy

Post on 11-May-2015

700 Views

Category:

Technology

4 Downloads

Preview:

Click to see full reader

DESCRIPTION

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.

TRANSCRIPT

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