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.

Let your tests drive your code

1,600 views

Published on

Test-driven Development (TDD) is still a subject all developers agree is a great thing, but never get around to actually doing it for many reasons. In this workshop, I use real-world business requirements on legacy code for which we need to fix bugs and add features, but we’re doing it in a TDD way.

Published in: Engineering
  • Be the first to comment

  • Be the first to like this

Let your tests drive your code

  1. 1. Let your tests drive your development An in2it workshop in it2PROFESSIONAL PHP SERVICES
  2. 2. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Agenda 2 Introduction TDD from scratch TDD with legacy app Additional tips Recap
  3. 3. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Agenda 3 Introduction TDD from scratch TDD with legacy app Additional tips Recap
  4. 4. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development What is test-driven development (TDD)? Write unit tests first They will fail Write functional code in accordance of the tests Your tests will structure the way you write your code Re-run your tests again They should pass 4
  5. 5. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development System’s Check 5
  6. 6. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Some conventions PHPUnit was installed using composer All vendor packages were installed with the code base Running PHPUnit with ./vendor/bin/phpunit If you use a different approach, make sure it works for you GIT is used for the exercises, make sure you know about checking out branches reading git logs In the slides I left out comments to save space 6
  7. 7. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Functional requirement Write a small PHP class with a method that will return the string “Hello World!” 7
  8. 8. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example PHPUnit test <?php namespace AppTest; use PHPUnitFrameworkTestCase; 
 use AppHelloWorld; class HelloWorldTest extends TestCase 
 { public function testAppOutputsHelloWorld() { $helloWorld = new HelloWorld(); $expectedAnswer = $helloWorld->sayHello(); $this->assertSame('Hello World!', $expectedAnswer); } } 8
  9. 9. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Running unit tests 9
  10. 10. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Writing the code <?php namespace App; class HelloWorld 
 { 
 public function sayHello(): string 
 { 
 return 'Hello World!'; 
 } 
 } 10
  11. 11. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Re-run unit tests 11
  12. 12. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Change requests 13
  13. 13. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Change request Update our small PHP class method that will allow an argument and will return the string “Hello <arg>!” where <arg> is the argument provided. 14
  14. 14. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Options 15
  15. 15. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Why change the existing test? Code change so test has to change New requirements change the test goal 16
  16. 16. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Why not change the existing test? Code requirement change so new test is required We don’t want to change existing requirements Prevent BC breaks New test will cover changing requirements 17
  17. 17. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example New test case public function testAppOutputsHelloArgument() 
 { 
 $helloWorld = new HelloWorld(); 
 $expectedAnswer = $helloWorld->sayHello('unit testers'); 
 $this->assertSame('Hello unit testers!', $expectedAnswer); 
 } 18
  18. 18. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Running unit tests 19
  19. 19. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example
 Changing the code <?php namespace App; class HelloWorld 
 { 
 public function sayHello(string $arg): string 
 { 
 return 'Hello ' . $arg . '!'; 
 } 
 } 20
  20. 20. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Re-run the tests 21
  21. 21. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development We introduced an error now! 22
  22. 22. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Finding the bug <?php namespace App; class HelloWorld 
 { 
 public function sayHello(string $arg): string 
 { 
 return 'Hello ' . $arg . '!'; 
 } 
 } 23 No default value
  23. 23. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Fixing failure <?php namespace App; class HelloWorld { public function sayHello(string $arg = 'World'): string { return 'Hello ' . $arg . '!'; } } 24
  24. 24. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Re-run the tests 25
  25. 25. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Recap Write test based on functionality (and run test) Write code based on functionality (and re-run test) Write new test based on changed functionality (and re-run tests) Change code based on functionality (and re-run tests) Update code until all tests are passing 27
  26. 26. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Simple example Get the source code Go to the project folder and use the following commands git checkout simple-example
 ./vendor/bin/phpunit All files will be there, so review them closely 28
  27. 27. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development29 Exercise New change requirements Security is important, so we need to validate the given argument so it only accepts string type values. If something other than a string is provided, an exception should be raised. 10 minutes
  28. 28. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Agenda 30 Introduction TDD from scratch TDD with legacy app Additional tips Recap
  29. 29. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Project “TodoToDone” Project “TodoToDone” is a simple todo tool, tracking the tasks that you need to do. It should provide the following features: List open tasks sorted newest to oldest Create a new task (label and description) Update an existing task Mark task as done in the overview list Remove task marked as done 31
  30. 30. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Requirements as skeleton <?php namespace AppTestService; use PHPUnitFrameworkTestCase; class TaskServiceTest extends TestCase {     public function testServiceReturnsListOfTasks()     {         // List open tasks sorted newest to oldest     }     public function testServiceCanAddNewTask()     {         // Create a new task (label and description)     }     public function testServiceCanUpdateExistingTask()     {         // Update an existing task     }     public function testServiceCanMarkTaskAsDone()     {         // Mark task as done in the overview list     }     public function testServiceCanRemoveTaskMarkedAsDone()     {         // Remove task marked as done     } } 32
  31. 31. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Don’t start writing test yet! Unit testing is about looking at a specific task from every angle Define use and edge cases and add them as additional tests 33
  32. 32. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Example edge cases public function testServiceWillThrowRuntimeExceptionWhenStorageFailsToFetchTaskList() {     // Throw a runtime exception when connection to storage fails for fetching task list } public function testServiceWillThrowInvalidArgumentExceptionWhenInvalidTaskIsAdded() {     // Throw an invalid argument exception for invalid task when adding } public function testServiceWillThrowRuntimeExceptionWhenStorageFails() {     // Throw a runtime exception when storage of task fails } public function testServiceWillThrowDomainExceptionWhenTaskWasMarkedAsDoneWhenMarkingTaskAsDone() {     // Throw a domain exception when a task was already marked as done  } 34
  33. 33. Question Why am I using very long and explicit method names for my test methods?
  34. 34. Answer To have human readable documentation about the features we’re developing and testing. ./vendor/bin/phpunit --testdox
  35. 35. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development TestDox output PHPUnit 6.1.3 by Sebastian Bergmann and contributors. AppTestServiceTaskService [ ] Service returns list of tasks [ ] Service can add new task [ ] Service throws exception if task was not found [ ] Service can find task [ ] Service can remove task [ ] Service can update existing task [ ] Service can mark task as done [ ] Service can remove task marked as done [ ] Service will throw type error when invalid task is added [ ] Service will throw domain exception when done task gets marked done 37
  36. 36. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development38 Exercise Complete the test cases git checkout tdd-ex1 Go and check out branch tdd-ex1 where you will find the code as we’ve seen thus far. Pro tip: complete 1 test and commit, this way you also learn to commit small and commit often. 20 minutes
  37. 37. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development How we’re approaching this We need to “prepare” our test class Create a “setUp” method to create a fixture Create a “tearDown” method to unset the fixture Implement first test “testServiceReturnsListOfTasks” Making use of fixture to mimic actual behaviour Create class interfaces for structure Implement concrete class 39
  38. 38. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Preparing our 1st test 40
  39. 39. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development class TaskServiceTest extends TestCase {     protected $taskGateway;     protected function setUp()     {         parent::setUp();         $taskEntity = $this->getMockBuilder(TaskEntityInterface::class)             ->setMethods(['getId', 'getLabel', 'getDescription', 'isDone', 'getCreated', 'getModified'])->getMock();         $taskEntry1 = clone $taskEntity;         $taskEntry1->method('getId')->willReturn('123');         $taskEntry1->method('getLabel')->willReturn('Task #123');         $taskEntry1->method('getDescription')->willReturn('#123: This is task 123');         $taskEntry1->method('isDone')->willReturn(false);         $taskEntry1->method('getCreated')->willReturn(new DateTime('2017-03-21 07:53:24'));         $taskEntry1->method('getModified')->willReturn(new DateTime('2017-03-21 08:16:53'));         $taskEntry2 = clone $taskEntity;         $taskEntry3 = clone $taskEntity;         $taskCollection = new SplObjectStorage();         $taskCollection->attach($taskEntry3);         $taskCollection->attach($taskEntry2);         $taskCollection->attach($taskEntry1);         $taskGateway = $this->getMockBuilder(TaskGatewayInterface::class)->setMethods(['fetchAll'])->getMock();         $taskGateway->expects($this->any())->method('fetchAll')->willReturn($taskCollection);         $this->taskGateway = $taskGateway;     }     /* ... */ } 41
  40. 40. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development class TaskServiceTest extends TestCase {     /* ... */     protected function tearDown()     {         unset ($this->taskGateway);     }     /* ... */ } 42
  41. 41. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development class TaskServiceTest extends TestCase {     /* ... */     /**      * List open tasks sorted newest to oldest      *      * @covers TaskService::fetchAll      */     public function testServiceReturnsListOfTasks()     {         $taskService = new TaskService($this->taskGateway);         $taskList = $taskService->getAllTasks();         $this->assertInstanceOf(Iterator::class, $taskList);         $this->assertGreaterThan(0, count($taskList));         $taskList->rewind();         $previous = null;         while ($taskList->valid()) {             if (null !== $previous) {                 $current = $taskList->current();                 $this->assertTrue($previous->getCreated() > $current->getCreated());             }             $previous = $taskList->current();             $taskList->next();         }     }     /* ... */ } 43
  42. 42. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Creating gateway interface <?php namespace AppModel; interface TaskGatewayInterface {     /**      * Fetch all tasks from the back-end storage      * @return Iterator      */     public function fetchAll(): Iterator; } 44
  43. 43. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Creating entity interface <?php namespace AppModel; interface TaskEntityInterface {     public function getId(): string;     public function getLabel(): string;     public function getDescription(): string;     public function isDone(): bool;     public function getCreated(): DateTime;     public function getModified(): DateTime; } 45
  44. 44. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development46
  45. 45. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development47 Exercise Check out the finished test cases git checkout tdd-ex2 Go and check out branch tdd-ex2 where you will find the completed test cases. Make sure you also check out the GIT logs as I used 27 commits to explain what was happening and why! 10 minutes
  46. 46. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development protected function setUp() { parent::setUp(); // We create a mock object $taskEntity = $this->getMockBuilder(TaskEntityInterface::class) ->setMethods(['getId', 'getLabel', 'getDescription', 'isDone', 'getCreated', 'getModified', 'setLabel', 'setDone']) ->getMock(); $taskEntry1 = clone $taskEntity; $taskEntry1->method('getId')->willReturn('123'); $taskEntry1->method('getLabel')->willReturn('Task #123'); $taskEntry1->method('getDescription')->willReturn('#123: This is task 123'); $taskEntry1->method('isDone')->willReturn(false); $taskEntry1->method('getCreated')->willReturn(new DateTime('2017-03-21 07:53:24')); $taskEntry1->method('getModified')->willReturn(new DateTime('2017-03-21 08:16:53')); $taskEntryUpdate = clone $taskEntity; $taskEntryUpdate->method('getId')->willReturn('123'); $taskEntryUpdate->method('getLabel')->willReturn('Task #123: Update from service'); $taskEntryUpdate->method('getDescription')->willReturn('#123: This is task 123'); $taskEntryUpdate->method('isDone')->willReturn(false); $taskEntryUpdate->method('getCreated')->willReturn(new DateTime('2017-03-21 07:53:24')); $taskEntryUpdate->method('getModified')->willReturn(new DateTime('now')); $taskEntry2 = clone $taskEntity; $taskEntry2->method('getId')->willReturn('456'); $taskEntry2->method('getLabel')->willReturn('Task #456'); $taskEntry2->method('getDescription')->willReturn('#456: This is task 456'); $taskEntry2->method('isDone')->willReturn(true); $taskEntry2->method('getCreated')->willReturn(new DateTime('2017-03-22 07:53:24')); $taskEntry2->method('getModified')->willReturn(new DateTime('2017-03-22 08:16:53')); $taskEntry3 = clone $taskEntity; $taskEntry3->method('getId')->willReturn('789'); $taskEntry3->method('getLabel')->willReturn('Task #789'); $taskEntry3->method('getDescription')->willReturn('#789: This is task 789'); $taskEntry3->method('isDone')->willReturn(false); $taskEntry3->method('getCreated')->willReturn(new DateTime('2017-04-23 07:53:24')); $taskEntry3->method('getModified')->willReturn(new DateTime('2017-04-23 08:16:53')); $taskEntryDone = clone $taskEntity; $taskEntryDone->method('getId')->willReturn('789'); $taskEntryDone->method('getLabel')->willReturn('#789'); $taskEntryDone->method('getDescription')->willReturn('#789: This is task 789'); $taskEntryDone->method('isDone')->willReturn(true); $taskEntryDone->method('getCreated')->willReturn(new DateTime('2017-04-23 07:53:24')); $taskEntryDone->method('getModified')->willReturn(new DateTime('now')); 48
  47. 47. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Magic happening in setUp Ideal place to set things up (using fixtures) Stub is shared among different test methods Now all is ready to be implemented as we secured the code-base 49
  48. 48. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Project status AppTestServiceTaskService [x] Service returns list of tasks [x] Service can add new task [x] Service throws exception if task was not found [x] Service can find task [x] Service can remove task [x] Service can update existing task [x] Service can mark task as done [x] Service can remove task marked as done [x] Service will throw type error when invalid task is added [x] Service will throw domain exception when done task gets marked done 50
  49. 49. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development51 Exercise Check out the finished test cases git checkout tdd-ex3 Go and check out branch tdd-ex3 where you will find the other completed test cases. 15 minutes
  50. 50. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development52
  51. 51. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development TextDox output PHPUnit 6.1.3 by Sebastian Bergmann and contributors. AppTestModelTaskEntity [x] Task entity is empty at construction [x] Task entity throws error when constructed with wrong type of arguments [x] Task entity throws exception when constructed with wrong arguments [x] Task entity accepts correct arguments AppTestModelTaskGateway [x] Fetch all returns iterator object [x] Gateway can add task entity [x] Find returns null when nothing found [x] Find returns task entity when result is found [x] Gateway can remove task entity [x] Gateway can update task entity AppTestServiceTaskService [x] Service returns list of tasks [x] Service can add new task [x] Service throws exception if task was not found [x] Service can find task [x] Service can remove task [x] Service can update existing task [x] Service can mark task as done [x] Service can remove task marked as done [x] Service will throw type error when invalid task is added [x] Service will throw domain exception when done task gets marked done 53
  52. 52. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development54 Exercise Check out the web app git checkout tdd-ex4 php -S localhost:8000 -t web web/index.php Go and check out branch tdd-ex4 where you will find the other completed test cases. Run the web application using PHP’s build-in web server to see how the app is behaving. 15 minutes
  53. 53. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development55
  54. 54. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development56
  55. 55. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development57
  56. 56. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development58
  57. 57. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development59 Conclusion Let’s recap what has happened here Writing test-first gives you a clean scope of what your code should do. You have a more precise code-base that’s easy to maintain, upgrade and is independent.
  58. 58. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Agenda 60 Introduction TDD from scratch TDD with legacy app Additional tips Recap
  59. 59. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Legacy challenges Not (always) written with testing in mind Dependencies make it hard to change code Refactoring is often required before proper testing can start For refactoring tests are required to ensure the refactored code behaves the same! 61
  60. 60. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Example project: EPESI 62
  61. 61. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development63
  62. 62. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Tests?!? 64
  63. 63. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Tests?!? 64
  64. 64. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development How to get started? 65
  65. 65. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Getting ready to test! <?xml version="1.0" encoding="UTF-8" ?> <phpunit bootstrap="vendor/autoload.php" colors="true" stopOnErrors="true" stopOnFailures="true"> <testsuites> <testsuite name="App unit tests"> <directory suffix="php">tests</directory> </testsuite> </testsuites> <filter> <whitelist> <directory suffix="php">src</directory> </whitelist> </filter> <logging> <log type="coverage-html" target="build/coverage" lowUpperBound="35" highLowerBound="70"/> </logging> </phpunit> 66
  66. 66. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development ModuleManager::module_install /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 67
  67. 67. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Testing happily… <?php use PHPUnitFrameworkTestCase; require_once __DIR__ . '/../src/ModuleManager.php'; class ModuleManagerTest extends TestCase {     /**      * @covers ModuleManager::include_install      */     public function testModuleManagerCanLoadMailModule()     {         $result = ModuleManager::include_install('Mail');         $this->assertTrue($result);     } } 68
  68. 68. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development69
  69. 69. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Comment out the test <?php use PHPUnitFrameworkTestCase; require_once __DIR__ . '/../src/ModuleManager.php'; class ModuleManagerTest extends TestCase {     /**      * @covers ModuleManager::include_install      */     /*public function testModuleManagerCanLoadMailModule()     {         $result = ModuleManager::include_install('Mail');         $this->assertTrue($result);     }*/ } 70
  70. 70. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Let’s look again, more closely /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 71
  71. 71. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Let’s look again, more closely /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 71
  72. 72. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Let’s look again, more closely /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 71
  73. 73. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Let’s look again, more closely /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 71
  74. 74. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Let’s look again, more closely /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 71
  75. 75. https://www.flickr.com/photos/marcgbx/7803086292
  76. 76. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Test first condition /**   * @covers ModuleManager::include_install   */  public function testReturnImmediatelyWhenModuleAlreadyLoaded()  {      $module = 'Foo_Bar';      ModuleManager::$modules_install[$module] = 1;      $result = ModuleManager::include_install($module);      $this->assertTrue($result);      $this->assertCount(1, ModuleManager::$modules_install);  } 73
  77. 77. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development74
  78. 78. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development75
  79. 79. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development76 https://www.flickr.com/photos/christian_johannesen/2248244786
  80. 80. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Test second condition /**   * @covers ModuleManager::include_install   */  public function testReturnWhenModuleIsNotFound()  {      $module = 'Foo_Bar';      $result = ModuleManager::include_install($module);      $this->assertFalse($result);      $this->assertEmpty(ModuleManager::$modules_install);  } 77
  81. 81. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development78
  82. 82. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 79
  83. 83. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 79 self::$modules_install[$module_class_name]
  84. 84. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Add a “setUp” method class ModuleManagerTest extends TestCase {     protected function setUp()     {         ModuleManager::$modules_install = [];     }          /* ... */ } 80
  85. 85. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development81
  86. 86. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development82 https://www.flickr.com/photos/evaekeblad/14780090550
  87. 87. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Testing third condition /**  * @covers ModuleManager::include_install  * @expectedException Error  */ public function testTriggerErrorWhenInstallClassDoesNotExists() {     $module = 'EssClient';     $result = ModuleManager::include_install($module);     $this->fail('Expecting loading module ' . $module . ' would trigger an error'); } 83
  88. 88. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development84
  89. 89. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 85
  90. 90. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development /**  * Includes file with module installation class.  *  * Do not use directly.  *  * @param string $module_class_name module class name - underscore separated  */ public static final function include_install($module_class_name) {     if(isset(self::$modules_install[$module_class_name])) return true;     $path = self::get_module_dir_path($module_class_name);     $file = self::get_module_file_name($module_class_name);     $full_path = 'modules/' . $path . '/' . $file . 'Install.php';     if (!file_exists($full_path)) return false;     ob_start();     $ret = require_once($full_path);     ob_end_clean();     $x = $module_class_name.'Install';     if(!(class_exists($x, false)) ||  !array_key_exists('ModuleInstall',class_parents($x)))         trigger_error('Module '.$path.': Invalid install file',E_USER_ERROR);     self::$modules_install[$module_class_name] = new $x($module_class_name);     return true; } 85 if (!file_exists($full_path)) return false;
  91. 91. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Our current file structure |-- ModuleManager.php `-- modules     |-- EssClient     |   `-- EssClient.php     |-- IClient     |   `-- IClientInstall.php     `-- Mail         `-- MailInstall.php 86
  92. 92. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development87 https://www.flickr.com/photos/sis/2497912343 Dead Code
  93. 93. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development So this test… /**  * @covers ModuleManager::include_install  * @expectedException Error  */ public function testTriggerErrorWhenInstallClassDoesNotExists() {     $module = 'EssClient';     $result = ModuleManager::include_install($module);     $this->fail('Expecting loading module ' . $module . ' would trigger an error'); } 88
  94. 94. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development … changes into this test /**  * @covers ModuleManager::include_install  */ public function testTriggerErrorWhenInstallClassDoesNotExists() {     $module = 'EssClient';     $result = ModuleManager::include_install($module);     $this->assertFalse($result); } 89
  95. 95. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development90 https://www.flickr.com/photos/fragiletender/5332586299
  96. 96. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Our current file structure |-- ModuleManager.php `-- modules     |-- EssClient     |   `-- EssClient.php     |-- IClient     |   `-- IClientInstall.php     `-- Mail         `-- MailInstall.php 91
  97. 97. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Testing fourth condition /**  * @covers ModuleManager::include_install  * @expectedException Error  */ public function testTriggerErrorWhenInstallClassIsNotRegistered() {     $module = 'IClient';     $result = ModuleManager::include_install($module);     $this->fail('Expecting loading module ' . $module . ' would trigger an error'); } 92
  98. 98. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development93
  99. 99. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development94
  100. 100. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Completing all tests 95
  101. 101. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Remove comment tags /**  * @covers ModuleManager::include_install  */ public function testModuleManagerCanLoadMailModule() {     $result = ModuleManager::include_install('Mail');     $this->assertTrue($result); } 96
  102. 102. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development97
  103. 103. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development98
  104. 104. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Recap Testing legacy code is not easy, but still possible Approach the source-code with a bail-first approach Make sure you can “bail” the method as fast as possible Start with the most important part of your code Ask yourself “What costs us money if it breaks” ➡ test that first! 99
  105. 105. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development100 Exercise Check out these tests git checkout legacy-0.1 Go and check out branch legacy-0.1 and analyse the tests. If you have XDebug installed, you can run PHPUnit with code coverage. 15 minutes
  106. 106. What to do? If your code doesn’t return values
  107. 107. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development /**  * Process Bank Payment files  */ public function processBankPayments() {     $this->getLogger()->log('Starting bank payment process', Zend_Log::INFO);     foreach ($this->_getBankFiles() as $bankFile) {         $bankData = $this->_processBankFile($bankFile);         $this->getLogger()->log('Processing ' . $bankData->transactionId, Zend_Log::DEBUG);         /** @var Contact_Model_Contact $contact */         $contact = $this->getMapper('Contact_Model_Mapper_Contact')             ->findContactByBankAccount($bankData->transactionAccount);         if (null !== $contact) {             $this->getLogger()->log(sprintf('Found contact "%s" for bank account %s',                  $contact->getName(),$bankData->transactionAccount             ), Zend_Log::DEBUG);             $data = array (                 'amount' => $bankData->transactionAmount,                 'payment_date' => $bankData->transactionDate             );             $this->getMapper('Invoice_Model_Mapper_Payments')                 ->updatePayment($data, array ('contact_id = ?' => $contact->getContactId()));             $this->_moveBankFile($bankFile,                 $this->getPath() . DIRECTORY_SEPARATOR . self::PROCESS_SUCCEEDED             );         } else {             $this->getLogger()->log(sprintf(                 'Could not match bankaccount "%s" with a contact',                 $bankData->transactionAccount             ), Zend_Log::WARN);             $this->_moveBankFile($bankFile,                 $this->getPath() . DIRECTORY_SEPARATOR . self::PROCESS_FAILED             );         }     } } 102
  108. 108. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development103
  109. 109. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development class Payments_Service_As400Test extends PHPUnitFrameworkTestCase {     public function testProcessingBankPayments()     {         $contact = $this->getMockBuilder(Contact_Model_Contact::class)             ->setMethods(['getContactId', 'getName'])             ->getMock();         $contact->expects($this->any())             ->method('getContactId')             ->will($this->returnValue(1));         $contact->expects($this->any())             ->method('getName')             ->will($this->returnValue('Foo Bar'));         $contactMapper = $this->getMockBuilder(Contact_Model_Mapper_Contact::class)             ->setMethods(['findContactByBankAccount'])             ->getMock();         $contactMapper->expects($this->any())             ->method('findContactByBankAccount')             ->will($this->returnValue($contact));         $paymentsMapper = $this->getMockBuilder(Invoice_Model_Mapper_Payments::class)             ->setMethods(['updatePayment'])             ->getMock();         $logMock = new Zend_Log_Writer_Mock();         $logger = new Zend_Log();         $logger->setWriter($logMock);         $logger->setPriority(Zend_Log::DEBUG);         $as400 = new Payments_Service_As400();         $as400->addMapper($contactMapper, Contact_Model_Mapper_Contact::class)             ->addMapper($paymentsMapper, Invoice_Model_Mapper_Payments::class)             ->setPath(__DIR__ . DIRECTORY_SEPARATOR . '_files')             ->setLogger($logger);         $as400->processBankPayments();         $this->assertCount(3, $logMock->events);         $this->assertEquals('Processing 401341345', $logMock->events[1]);         $this->assertEquals(             'Found contact "Foo Bar" for bank account BE93522511513933',             $logMock->events[2]         );     } } 104
  110. 110. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Let’s focus on mocks first $contact = $this->getMockBuilder(Contact_Model_Contact::class)     ->setMethods(['getContactId', 'getName'])     ->getMock(); $contact->expects($this->any())     ->method('getContactId')     ->will($this->returnValue(1)); $contact->expects($this->any())     ->method('getName')     ->will($this->returnValue('Foo Bar')); $contactMapper = $this->getMockBuilder(Contact_Model_Mapper_Contact::class)     ->setMethods(['findContactByBankAccount'])     ->getMock(); $contactMapper->expects($this->any())     ->method('findContactByBankAccount')     ->will($this->returnValue($contact)); $paymentsMapper = $this->getMockBuilder(Invoice_Model_Mapper_Payments::class)     ->setMethods(['updatePayment'])     ->getMock(); 105
  111. 111. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Write the log mock $logMock = new Zend_Log_Writer_Mock(); $logger = new Zend_Log(); $logger->setWriter($logMock); $logger->setPriority(Zend_Log::DEBUG); $as400 = new Payments_Service_As400(); $as400->addMapper($contactMapper, Contact_Model_Mapper_Contact::class)     ->addMapper($paymentsMapper, Invoice_Model_Mapper_Payments::class)     ->setPath(__DIR__ . DIRECTORY_SEPARATOR . '_files')     ->setLogger($logger); $as400->processBankPayments(); $this->assertCount(3, $logMock->events); $this->assertEquals('Processing 401341345', $logMock->events[1]); $this->assertEquals(     'Found contact "Foo Bar" for bank account BE93522511513933',     $logMock->events[2] ); 106
  112. 112. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development107
  113. 113. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development108 Exercise Check out the web app git checkout legacy-0.2 Go and check out branch legacy-0.2 where you will find the example test case. 10 minutes
  114. 114. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Privates Exposed 109 http://www.slashgear.com/former-tsa-agent-admits-we-knew-full-body-scanners-didnt-work-31315288/
  115. 115. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Direct access forbidden?!? <?php defined("_VALID_ACCESS") || die('Direct access forbidden'); /**  * This class provides dependency requirements  * @package epesi-base  * @subpackage module   */ class Dependency {     private $module_name;     private $version_min;     private $version_max;     private $compare_max;     private function __construct( $module_name, $version_min, $version_max, $version_max_is_ok = true) {         $this->module_name = $module_name;         $this->version_min = $version_min;         $this->version_max = $version_max;         $this->compare_max = $version_max_is_ok ? '<=' : '<';     }     /** ... */ } 110
  116. 116. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Don’t touch my junk! 111 https://www.flickr.com/photos/caseymultimedia/5412293730
  117. 117. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development House of Reflection https://www.flickr.com/photos/tabor-roeder/8250770115
  118. 118. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Let’s do this… <?php require_once 'include.php'; class DependencyTest extends PHPUnit_Framework_TestCase {     public function testConstructorSetsProperSettings()     {         require_once 'include/module_dependency.php';         // We have a problem, the constructor is private!     } } 113
  119. 119. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Using static method works too $params = array (     'moduleName' => 'Foo_Bar',     'minVersion' => 0,     'maxVersion' => 1,     'maxOk' => true, ); // We use a static method for this test $dependency = Dependency::requires_range(     $params['moduleName'],     $params['minVersion'],     $params['maxVersion'],     $params['maxOk'] ); // We use reflection to see if properties are set correctly $reflectionClass = new ReflectionClass('Dependency'); 114
  120. 120. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Asserting private properties // Let's retrieve the private properties $moduleName = $reflectionClass->getProperty('module_name'); $moduleName->setAccessible(true); $minVersion = $reflectionClass->getProperty('version_min'); $minVersion->setAccessible(true); $maxVersion = $reflectionClass->getProperty('version_max'); $maxVersion->setAccessible(true); $maxOk = $reflectionClass->getProperty('compare_max'); $maxOk->setAccessible(true); // Let's assert $this->assertEquals($params['moduleName'], $moduleName->getValue($dependency),     'Expected value does not match the value set’); $this->assertEquals($params['minVersion'], $minVersion->getValue($dependency),     'Expected value does not match the value set’); $this->assertEquals($params['maxVersion'], $maxVersion->getValue($dependency),     'Expected value does not match the value set’); $this->assertEquals('<=', $maxOk->getValue($dependency),     'Expected value does not match the value set'); 115
  121. 121. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development116
  122. 122. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Agenda 117 Introduction TDD from scratch TDD with legacy app Additional tips Recap
  123. 123. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development PHPUnit requires basic PHP! Sometimes the challenge lies within PHP instead of direct PHPUnit Testing is simple, coding is hard! Testing is all about asserting that an actual process matches an expected result, so make sure you cover your expectations and test against those expectations PHP functionality you need to know: Reflection Streams System and PHP executions (e.g. “eval”, “passthru”, …) 118
  124. 124. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development If you don’t know the destination… Start testing with what you know Work your way up 119
  125. 125. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development “But my code is too crappy…” 120
  126. 126. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development For “untestable” code Write out the functionality in tests Create a class providing this functionality (service, model, …) Slowly move your existing code over to use the “cleaner” code Bonus: you’ve got it already tested 121
  127. 127. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Agenda 122 Introduction TDD from scratch TDD with legacy app Additional tips Recap
  128. 128. Everything is testable! Not always easy, but always possible
  129. 129. Write your tests first Write your code based on your tests
  130. 130. Use code coverage as guide It shows your progress through the code
  131. 131. Be creative! Sometimes PHP can help out
  132. 132. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Recommended reading 127
  133. 133. GitHub Repo github.com/in2it-training/tdd-workshop
  134. 134. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development Thank you 129 https://www.flickr.com/photos/drewm/3191872515
  135. 135. in2it training - www.in2it.be - @in2itvof - #in2tdd Let your tests drive your development130 in it2PROFESSIONAL PHP SERVICES Michelangelo van Dam Zend Certified Engineer contact@in2it.be - www.in2it.be - T in2itvof - F in2itvof Quality Assurance Zend Framework 3 Consulting Disaster Recovery Development Workflow Enterprise PHP Training Mentoring Our expertise services

×