Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Tests and Testability: Apex Structure and Strategy

1,490 views

Published on

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.

Published in: Technology, Business
  • Be the first to comment

Tests and Testability: Apex Structure and Strategy

  1. 1. Tests and Testability Apex Structure and Strategy Stephen Willcock, FinancialForce.com, Director of Product Innovation @stephenwillcock
  2. 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. 3. Tests and Testability - overview Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  4. 4. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  5. 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!
  6. 6. Test pyramid Fewer tests More numerous tests More complex tests Less complex tests
  7. 7. Test pyramid
  8. 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. 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. 10. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  11. 11. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  12. 12. Unit test principles - what is a unit? The smallest testable chunk of code Independent from other units and systems Uno
  13. 13. Unit test principles - what is a unit?
  14. 14. Unit test principles - what is a unit?
  15. 15. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  16. 16. Unit test principles - isolation https:// Uno Uno Uno Uno @ Uno Trigger Validation Rule Workflow Rule Uno Managed Apex Database Related Data …further dependencies
  17. 17. Unit test principles - isolation Uno Uno Mocked resources Database
  18. 18. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  19. 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
  20. 20. Unit test principles - saturation
  21. 21. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  22. 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. 23. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  24. 24. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  25. 25. Unit test principles - testability
  26. 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. 27. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  28. 28. SObject fabrication Insert supporting data Evaluate and assert Read data Insert test data Database commit Trigger fires Triggered record update Unit test
  29. 29. SObject fabrication - the unit trigger OpportunityLineItems on OpportunityLineItem (before insert) { for(OpportunityLineItem item : Trigger.new) { if(item.Description==null) item.Description = 'foo'; } }
  30. 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. 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. 32. SObject fabrication - the revised unit trigger OpportunityLineItems on OpportunityLineItem (before insert) { new OpportunityLineItemsTriggerHandler().beforeInsert( Trigger.new ); } Testable code: break up the Trigger
  33. 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. 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. 35. SObject fabrication #2 - the unit public class OpportunityService { public void adjust(OpportunityLineItem oli) { oli.UnitPrice += (oli.UnitPrice * oli.Opportunity.Account.Factor__c); } }
  36. 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. 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. 38. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  39. 39. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  40. 40. Loose type coupling Use inheritance to “loosen” a relationship •Interface •Superclass Substitute a mock “sibling implementation” during unit tests
  41. 41. Loose type coupling Consumer Provider OrderController (Custom Controller) Aggregator OrderController.Item Unit test
  42. 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. 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. 44. Loose type coupling - the revised provider public class Aggregator { public interface IItem { Decimal getValue(); } public void setItems(List<IItem> items) {…}
  45. 45. Loose type coupling - the revised provider public Decimal getSum() { Decimal result = 0; for(IItem item : items) result += item.getValue(); return result; } }
  46. 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. 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. 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. 49. Loose type coupling - what did we do? OrderController.Item Aggregator.IItem TItem Aggregator Production Unit Test
  50. 50. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  51. 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
  52. 52. Dependency injection Consumer Provider Opportunity Controller Opportunity Adjuster Opportunity o; OpportunityAdjuster a; a = new OpportunityAdjuster(); a.adjust(o); Unit test
  53. 53. Dependency injection Production usage Opportunity Adjuster Opportunity Controller Unit test usage Inject dependency TOpportunity Adjuster Opportunity Controller
  54. 54. Dependency injection - interface public interface IAdjustOpportunities { void adjust(Opportunity o); }
  55. 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. 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. 57. Dependency injection - consumer public class OpportunityController { IAdjustOpportunities adjuster; @testVisible OpportunityController( IAdjustOpportunities a, ApexPages.StandardController c ) { this.adjuster = a; … }
  58. 58. Dependency injection - consumer public OpportunityController( ApexPages.StandardController c) { this(new OpportunityAdjuster(), c); } … public void makeAdjustment() { adjuster.adjust(opp); }
  59. 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. 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. 61. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  62. 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. 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. 64. SObject Decoupler - the decoupler public virtual class OpportunityDecoupler { public virtual Boolean getIsClosed( Opportunity o ) { return o.IsClosed; } }
  65. 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. 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. 67. SObject Decoupler - the revised unit public void afterUpdate(List<Opportunity> items) { for(Opportunity item : items) { if(decoupler.getIsClosed(item)) { // do something } } } }
  68. 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. 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. 70. SObject Decoupler - the revised test OpportunitiesTriggerHandler handler = new OpportunitiesTriggerHandler(decoupler); handler.afterUpdate(new List<Opportunity>{o}); // test something }
  71. 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
  72. 72. SObject Decoupler - useful for… Mocking: •Formula fields •System fields •Rollup summary fields •Subselects • Select (Select ... From OpportunityLineItems) From Opportunity
  73. 73. Tests and Testability Testing strategy • Test pyramid Unit test principles • What is a unit? • Isolation • Saturation • Expectation Unit test techniques • Testability
  74. 74. Adapter - for managed classes IHandleLockingRules OpportunitiesTrigger Handler LockingRule Handler (wrapper) LockingRules. LockingRule Handler (managed class) Test LockingRuleHandler implements IHandleLockingRules
  75. 75. Adapter - managed class global class LockingRuleHandler static void handleTrigger()
  76. 76. Adapter - interface public interface IHandleLockingRules { void handleAfterUpdate(Map<Id,sObject> oldMap, Map<Id,sObject> newMap); }
  77. 77. Adapter - wrapper public class LockingRuleHandler implements IHandleLockingRules { public void handleAfterUpdate(Map<Id,sObject> oldMap, Map<Id,sObject> newMap) { LockingRules.LockingRuleHandler.handleTrigger(); } }
  78. 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. 79. Adapter - the unit public class OpportunitiesTriggerHandler { IHandleLockingRules lockingRules; @testVisible OpportunitiesTriggerHandler( IHandleLockingRules lr ) { this.lockingRules = lr; } public OpportunitiesTriggerHandler() { this(new LockingRuleHandler()); } Constructor injection
  80. 80. Adapter - the unit public void handleAfterUpdate(Map<Id,sObject> oldMap, Map<Id,sObject> newMap) { lockingRules.handleAfterUpdate(oldMap, newMap); }
  81. 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. 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. 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
  84. 84. Going forward… Tests and Testability on foobarforce.com Sample code on Github @stephenwillcock
  85. 85. Stephen Willcock Director of Product Innovation at FinancialForce. com @stephenwillcock

×