Successfully reported this slideshow.
Your SlideShare is downloading. ×

Testing the Untestable

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Loading in …3
×

Check these out next

1 of 48 Ad

More Related Content

Slideshows for you (20)

Similar to Testing the Untestable (20)

Advertisement

More from Mark Baker (20)

Recently uploaded (20)

Advertisement

Testing the Untestable

  1. 1. Testing the Untestable
  2. 2. Unit Tests are Great ! • Proof that your code does what it is supposed to do • Proof that refactoring code doesn’t break its functionality • Automated Regressions Tests • Verify that new code doesn’t break existing code • Unit Tests are a part of the documentation of the code
  3. 3. TDD is Great ! • Writing tests helps design how the code should work • Write tests as we define the public methods • All green tests tell us when the coding is complete • Bugfixing • Write test for bug • Fix bug so that test passes
  4. 4. Testing the Untestable
  5. 5. Testing the Untestable • 100% Code Coverage is over-rated • Prioritise writing Tests for the code that needs testing • Diminishing returns
  6. 6. Testing the Untestable • Test Doubles • Let the code run • Mock as little as possible • Database access • E-Mailing • Test values
  7. 7. Testing the Untestable • Constructor does Real Work • Indirect Collaborators • Global State & Singletons • Class Does Too Much
  8. 8. Testing the Untestable Constructor does Real Work • Difficulties • Creating/Initializing Collaborators • Communicating with other services, and logic to set up its own state • Forcing subclasses/mocks to inherit unwanted behaviour • Prevents instantiation or altering collaborators in the test.
  9. 9. Testing the Untestable Constructor does Real Work • Warning Signs • new keyword in a constructor • Static method calls in a constructor • Anything more than argument validation and field assignment in constructors • Object not fully initialized after the constructor finishes • Control flow (conditional or looping logic) in a constructor • Separate Initialisation methods
  10. 10. Testing the Untestable Constructor does Real Work • Why? • Inflexible and prematurely coupled design • Violates Single Responsibility Principle • Testing Directly is Difficult • Subclassing and Overriding to Test is Still Flawed • It Forces Collaborators on Us
  11. 11. Testing the Untestable Indirect Collaborators • Difficulties • “Middle Men” make change difficult • Unnecessary complexity and code bloat
  12. 12. Testing the Untestable Indirect Collaborators • Warning Signs • Objects are passed in but never used directly • Only used to get access to other objects • Suspicious names like context, environment, principal, container, or manager
  13. 13. Testing the Untestable Indirect Collaborators • Why? • Hard to identify the collaborator methods that we need to mock • Necessary to create mocks that return other mocks
  14. 14. Testing the Untestable Global State & Singletons • Difficulties • Global state mutated in one test can cause a subsequent or parallel test to fail unexpectedly. • Forcing subclasses/mocks to inherit unwanted behaviour • Prevents instantiation or altering collaborators in the test.
  15. 15. Testing the Untestable Global State & Singletons • Warning Signs • Use of Singletons • Using Static Properties or Methods • Using Registries or Service Locators • Static Initialisation Blocks
  16. 16. Testing the Untestable Global State & Singletons • Why? • Changes to global state can be made from anywhere • Unexpected initial state for individual tests
  17. 17. Testing the Untestable • Class Does Too Much • Difficulties • Tests for individual Methods can’t easily be isolated from other methods
  18. 18. Testing the Untestable • Class Does Too Much • Warning Signs • Class description uses the word “and” • Too many methods • Too many properties • Too many collaborators
  19. 19. Testing the Untestable The ideal number of arguments for a function is zero (niladic). Next comes one (monadic) followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification—and then shouldn't be used anyway. Bob Martin – “Clean Code”
  20. 20. Testing the Untestable public function __construct( MagentoFrameworkModelContext $context, MagentoFrameworkViewDesignInterface $design, MagentoFrameworkRegistry $registry, MagentoStoreModelAppEmulation $appEmulation, MagentoStoreModelStoreManagerInterface $storeManager, MagentoFrameworkAppRequestInterface $request, MagentoNewsletterModelTemplateFilter $filter, MagentoFrameworkAppConfigScopeConfigInterface $scopeConfig, MagentoNewsletterModelTemplateFactory $templateFactory, MagentoFrameworkFilterFilterManager $filterManager, array $data = [] ) { parent::__construct($context, $design, $registry, $appEmulation, $storeManager, $data); $this->_storeManager = $storeManager; $this->_request = $request; $this->_filter = $filter; $this->_scopeConfig = $scopeConfig; $this->_templateFactory = $templateFactory; $this->_filterManager = $filterManager; }
  21. 21. Testing the Untestable • Class Does Too Much • Why? • Difficult to modify behaviour of one responsibility without changing/breaking others • Muddled Responsibilities
  22. 22. Testing the Untestable namespace reporting; class dateCalculator { protected $currentDate; public function __construct() { $this->currentDate = new DateTime(); } public function weekToDate() { $startDate = clone $this->currentDate; $startDate->modify('last monday'); return new dateRange( $startDate->modify('midnight'), (clone $this->currentDate)->modify('midnight') ); } }
  23. 23. Testing the Untestable namespace reporting; class dateCalculatorTest extends PHPUnitFrameworkTestCase { public function testDateCalculatorWeek() { $dateCalculator = new dateCalculator(); $week = $dateCalculator->weekToDate(); $this->assertInstanceOf('reportingdateRange', $week); $this->assertInstanceOf('DateTime', $week->startDate); $this->assertEquals( DateTime::createFromFormat('Y-m-d|', '2018-01-08'), $week->startDate ); $this->assertInstanceOf('DateTime', $week->endDate); $this->assertEquals( DateTime::createFromFormat('Y-m-d|', '2018-01-10'), $week->endDate ); } }
  24. 24. Testing the Untestable • Dependency Injection • Inject either a date value or a DateTime object into the constructor
  25. 25. Testing the Untestable namespace reporting; class dateCalculator { protected $currentDate; public function __construct(string $date = 'now') { $this->currentDate = new DateTime($date); } public function weekToDate() { $startDate = clone $this->currentDate; $startDate->modify('last monday'); return new dateRange( $startDate->modify('midnight'), (clone $this->currentDate)->modify('midnight') ); } }
  26. 26. Testing the Untestable namespace reporting; class dateCalculator { protected $currentDate; public function __construct(DateTime $dto) { $this->currentDate = $dto; } public function weekToDate() { $startDate = clone $this->currentDate; $startDate->modify('last monday'); return new dateRange( $startDate->modify('midnight'), (clone $this->currentDate)->modify('midnight') ); } }
  27. 27. Testing the Untestable • Dependency Injection (Inversion of Control) • Inject either a date value or a DateTime object into the constructor • Not always an option if we can’t change the codebase • e.g. If we’re writing tests preparatory to refactoring • May require significant changes wherever the class we’re testing is used within the main codebase • Or introducing a Dependency Injection Container
  28. 28. Testing the Untestable • In the case of DateTime, we can modify the the DateTime constructor • If we’re using namespacing
  29. 29. Testing the Untestable class dateCalculator { protected $currentDate; public function __construct() { $this->currentDate = new DateTime("@" . time()); } .... }
  30. 30. Testing the Untestable namespace reporting; function time() { return strtotime('2018-01-10 01:23:45'); } class dateCalculatorTest extends PHPUnitFrameworkTestCase { public function testDateCalculatorWeek() { $dateCalculator = new dateCalculator(); $week = $dateCalculator->weekToDate(); .... Assertions } }
  31. 31. Testing the Untestable • In the case of DateTime, we can modify the the DateTime constructor • If we’re using namespacing • Overriding the time() function with a namespaced time() function • So we set our own value to be returned by a call to time()
  32. 32. Testing the Untestable • Move the assignment of $currentDate and the instantiation of the DateTime to a separate method called from the constructor • We can then mock that new method in our tests
  33. 33. Testing the Untestable class dateCalculator { protected $currentDate; protected function getCurrentDate() { return new DateTime(); } public function __construct() { $this->currentDate = $this->getCurrentDate(); } .... }
  34. 34. Testing the Untestable class dateCalculatorTest extends PHPUnitFrameworkTestCase { public function testDateCalculatorWeek() { $dateCalculator = $this->getMockBuilder('reportingdateCalculator') ->setMethods(['getCurrentDate']) ->getMock(); $dateCalculator->expects($this->once()) ->method('getCurrentDate') ->will( $this->returnValue(new DateTime('2018-01-10 01:23:45')) ); $week = $dateCalculator->weekToDate(); .... Assertions } }
  35. 35. Testing the Untestable • Or Use Reflection
  36. 36. Testing the Untestable class dateCalculator { protected $currentDate; protected function getCurrentDate() { return new DateTime(); } public function __construct() { $this->currentDate = $this->getCurrentDate(); } .... }
  37. 37. Testing the Untestable class dateCalculatorTest extends PHPUnitFrameworkTestCase { public function testDateCalculatorWeek() { $dateCalculator = $this->getMockBuilder('reportingdateCalculator') ->disableOriginalConstructor() ->setMethods(['getCurrentDate']) ->getMock(); $dateCalculator->expects($this->once()) ->method('getCurrentDate') ->will( $this->returnValue(new DateTime('2018-01-10 01:23:45')) ); .... } }
  38. 38. Testing the Untestable class dateCalculatorTest extends PHPUnitFrameworkTestCase { public function testDateCalculatorWeek() { .... // now call the constructor $reflectedClass = new ReflectionClass('reportingdateCalculator'); $constructor = $reflectedClass->getConstructor(); $constructor->invoke($dateCalculator); $week = $dateCalculator->weekToDate(); .... Assertions } }
  39. 39. Testing the Untestable • Anonymous Classes • http://php.net/manual/en/language.oop5.anonymous.php
  40. 40. Testing the Untestable class dateCalculatorTest extends PHPUnitFrameworkTestCase { public function testDateCalculatorWeek() { $dateCalculator = new class() extends dateCalculator { public function __construct() { // parent::__construct(); $this->currentDate = new DateTime('2018-01-10 01:23:45'); } }; $week = $dateCalculator->weekToDate(); .... Assertions } }
  41. 41. Testing the Untestable • https://github.com/MarkBaker/SpyMaster
  42. 42. Testing the Untestable public function testDateCalculatorWeek() { $dateCalculator = new dateCalculator(); $spy = (new SpyMasterSpyMaster($dateCalculator)) ->infiltrate(SpyMasterSpyMaster::SPY_READ_WRITE); $spy->currentDate = new DateTime('2018-01-10 01:23:45'); $week = $dateCalculator->weekToDate(); .... Assertions }
  43. 43. Testing the Untestable • Make sure we test against different dates using a phpunit dataprovider
  44. 44. Testing the Untestable /** * @dataProvider testDateCalculatorWeekProvider **/ public function testDateCalculatorWeek($baseline, $expectedResult) { $dateCalculator = new class($baseline) extends dateCalculator { public function __construct($baseline) { // parent::__construct(); $this->currentDate = new DateTime($baseline); } }; $week = $dateCalculator->weekToDate(); .... Assertions }
  45. 45. Testing the Untestable • Mockery • https://github.com/mockery/mockery • AspectMock • https://github.com/Codeception/AspectMock
  46. 46. Testing the Untestable • Mocking the Filesystem • https://github.com/mikey179/vfsStream
  47. 47. Testing the Untestable • Assertions for HTML Markup • https://github.com/stevegrunwell/phpunit-markup-assertions
  48. 48. Who am I? Mark Baker @Mark_Baker https://github.com/MarkBaker http://uk.linkedin.com/pub/mark-baker/b/572/171 http://markbakeruk.net

Editor's Notes

  • Problematic test only works on 10th January 2018
  • Best solution is to modify the code to inject the date into the constructor, either as a date-recognisable string
  • Or as a DateTime object
    This has the added advantage of allowing reports to be re-run for a particular historic date
  • Using an anonymous class to extend the class being tested and override the constructor

×