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.

TDD with PhpSpec

1,929 views

Published on

PhpSpec is a SpecBDD tool that enables you to use a TDD workflow that can transform the way you write PHP. In this session we will look at the TDD workflow and see how PhpSpec can be used to speed up your development; add regression safety, and improve your object-oriented design.

Published in: Technology
  • Be the first to comment

TDD with PhpSpec

  1. 1. TDD with PhpSpec with Ciaran McNulty at PHPNW 2015
  2. 2. TDD vs BDD (or are they the same?)
  3. 3. BDD is a second- generation, outside- in, pull-based, multiple- stakeholder… 1 Dan North
  4. 4. …multiple-scale, high-automation, agile methodology. 1 Dan North
  5. 5. BDD is the art of using examples in conversation to illustrate behaviour 1 Liz Keogh
  6. 6. Test Driven Development 4 Before you write your code, write a test that validates how it should behave 4 After you have written the code, see if it passes the test
  7. 7. Behaviour Driven Development 4 Before you write your code, describe how it should behave using examples 4 Then, Implement the behaviour you have described
  8. 8. SpecBDD with PhpSpec Describing individual classes
  9. 9. History 1.0 - Inspired by RSpec 4 Pádraic Brady and Travis Swicegood
  10. 10. History 2.0beta - Inspired by 1.0 4 Marcello Duarte and Konstantin Kudryashov (Everzet) 4 Ground-up rewrite 4 No BC in specs
  11. 11. Design principles 4 Optimise for descriptiveness 4 Encourage good design 4 Encourage TDD cycle 4 Do it the PHP way
  12. 12. History 2.0.0 to 2.2.0 - Steady improvement 4 Me 4 Christophe Coevoet 4 Jakub Zalas 4 Richard Miller 4 Gildas Quéméner 4 Luis Cordova + MANY MORE
  13. 13. Installation via Composer { "require-dev": { "phpspec/phpspec": "~2.0" }, "config": { "bin-dir": "bin" }, "autoload": {"psr-0": {"": "src"}} }
  14. 14. A requirement: We need a component that greets people
  15. 15. Describing object behaviour 4 We describe an object using a Specification 4 A specification is made up of Examples illustrating different scenarios Usage: phpspec describe [Class]
  16. 16. # spec/HelloWorld/GreeterSpec.php namespace specHelloWorld; use PhpSpecObjectBehavior; use ProphecyArgument; class GreeterSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('HelloWorldGreeter'); } }
  17. 17. Verifying object behaviour 4 Compare the real objects' behaviours with the examples Usage: phpspec run
  18. 18. # src/HelloWorld/Greeter.php namespace HelloWorld; class Greeter { }
  19. 19. An example for Greeter: When this greets, it should return "Hello"
  20. 20. # spec/HelloWorld/GreeterSpec.php class GreeterSpec extends ObjectBehavior { // ... function it_greets_by_saying_hello() { $this->greet()->shouldReturn('Hello'); } }
  21. 21. # src/HelloWorld/Greeter.php class Greeter { public function greet() { // TODO: write logic here } }
  22. 22. So now I write some code?
  23. 23. Fake it till you make it 4 Do the simplest thing that works 4 Only add complexity later when more examples drive it phpspec run --fake
  24. 24. # src/PhpDay/HelloWorld/Greeter.php class Greeter { public function greet() { return 'Hello'; } }
  25. 25. Describing values Matchers
  26. 26. Matchers # Equality $this->greet()->shouldReturn('Hello'); $this->sum(3,3)->shouldEqual(6); # Type $this->getEmail()->shouldHaveType('Email'); $this->getTime()->shouldReturnAnInstanceOf('DateTime'); # Fuzzy value matching $this->getSlug()->shouldMatch('/^[0-9a-z]+$/'); $this->getNames()->shouldContain('Tom');
  27. 27. Object state // isAdmin() should return true $this->getUser()->shouldBeAdmin(); // hasLoggedInUser() should return true $this->shouldHaveLoggedInUser();
  28. 28. Custom matchers function it_gets_json_with_user_details() { $this->getResponseData()->shouldHaveJsonKey('username'); } public function getMatchers() { return [ 'haveJsonKey' => function ($subject, $key) { return array_key_exists($key, json_decode($subject)); } ]; }
  29. 29. Wildcarding 4 In most cases you should know what arguments a method will be invoked with 4 If not, you can use wildcards $obj->doSomething(Argument::any())->will...; $obj->save(Argument::type(User::class))->will...;
  30. 30. Describing Exceptions
  31. 31. The shouldThrow matcher $this->shouldThrow(InvalidArgumentException::class) ->duringSave($user); or $this->shouldThrow(InvalidArgumentException::class) ->during(‘save’, [$user]);
  32. 32. Construction // new User(‘Ciaran’) $this->beConstructedWith('Ciaran'); // User::named(‘Ciaran’) $this->beConstructedThrough('named', ['Ciaran']); $this->beConstructedNamed('Ciaran'); // Testing constructor exceptions $this->shouldThrow(InvalidArgumentException::class) ->duringInstantiation();
  33. 33. Exercise 4 Install PhpSpec using composer 4 Describe a Calculator that takes two numbers and adds them together, by writing a few examples (using phpspec describe) 4 Test the specificaiton and see it fail (using phpspec run) 4 Implement the code so that the tests pass
  34. 34. The TDD Workflow
  35. 35. The Rules of TDD by Robert C Martin 1. Don’t write any code unless it is to make a failing test pass. 2. Don’t write any more of a test than is sufficient to fail. 3. Don’t write any more code than is sufficient to pass the one failing test.
  36. 36. Test - Describe the next behaviour 4 Think about a behaviour the object has that it doesn’t yet 4 Describe that behaviour in the form of a test 4 Find the simple or degenerate cases first 4 Don’t “Go for Gold”
  37. 37. Code - Make it pass 4 Code the most obvious or simplest working solution 4 Don’t overthink design - do that later 4 The test is failing! Get back to green ASAP
  38. 38. Refactor - Improve the design 4 Is there duplication? 4 What can be taken out? 4 Is the code clear and expressive? 4 The tests are passing so we can stop and think
  39. 39. Getting used to TDD
  40. 40. Pairing 4 Driver + Navigator roles 4 Driver controls the keyboard 4 Driver solves the immediate problems 4 Navigator checks the TDD rules are being enforced 4 Navigator thinks about what to test next, what future problems might come up
  41. 41. Kata 4 Short exercises to practise TDD 4 Solve an achievable problem in a fixed time 4 Throw away the code and do it again differently 4 Focus on the process not the problem You will probably not solve the problem on first attempt
  42. 42. Kata 4 String Calculator 4 Roman Numbers 4 Bowling 4 Tic-Tac-Toe 4 The Command Line Argument Parser 4 Prime Factors 4 Factorial 4 String Tokeniser
  43. 43. Kata - string calculator Design an object that takes a string expression and calculates an integer. 4 Empty string should evaluate to zero 4 Zero as a string should evaluate to zero 4 Numeric string should evaluate to that number 4 Space separated numbers should be added together 4 Whitespace separated numbers should be added together 4 Custom separator can be specified (e.g. ’[+]1+2+3’ -> 6)
  44. 44. Describing Collaboration
  45. 45. Another example for Greeter: When this greets a person called "Bob", it should return "Hello, Bob"
  46. 46. # spec/HelloWorld/GreeterSpec.php use HelloWorldPerson; class GreeterSpec extends ObjectBehavior { // ... function it_greets_a_person_by_name(Person $person) { $person->getName()->willReturn('Bob'); $this->greet($person)->shouldReturn('Hello, Bob'); } }
  47. 47. The Interface Segregation Principle: No client should be forced to depend on methods it does not use 1 Robert C Martin
  48. 48. # spec/HelloWorld/GreeterSpec.php use HelloWorldNamed; class GreeterSpec extends ObjectBehavior { // ... function it_greets_named_things_by_name(Named $named) { $named->getName()->willReturn('Bob'); $this->greet($named)->shouldReturn('Hello, Bob'); } }
  49. 49. # src/HelloWorld/Named.php namespace HelloWorld; interface Named { public function getName(); }
  50. 50. # src/HelloWorld/Greeter.php class Greeter { public function greet() { return 'Hello'; } }
  51. 51. Finally now we write some code!
  52. 52. # src/HelloWorld/Greeter.php class Greeter { public function greet(Named $named = null) { return 'Hello'; } }
  53. 53. # src/HelloWorld/Greeter.php class Greeter { public function greet(Named $named = null) { $greeting = 'Hello'; if ($named) { $greeting .= ', ' . $named->getName(); } return $greeting; } }
  54. 54. An example for a Person: When you ask a person named "Bob" for their name, they return "Bob"
  55. 55. # spec/HelloWorld/PersonSpec.php class PersonSpec extends ObjectBehavior { function it_returns_the_name_it_is_created_with() { $this->beConstructedWith('Bob'); $this->getName()->shouldReturn('Bob'); } }
  56. 56. # src/HelloWorld/Person.php class Person { public function __construct($argument1) { // TODO: write logic here } public function getName() { // TODO: write logic here } }
  57. 57. # src/HelloWorld/Person.php class Person implements Named { private $name; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } }
  58. 58. Another example for a Person: When a person named "Bob" changes their name to "Alice", when you ask their name they return "Alice"
  59. 59. # spec/HelloWorld/PersonSpec.php class PersonSpec extends ObjectBehavior { function it_returns_the_name_it_is_created_with() { $this->beConstructedWith('Bob'); $this->getName()->shouldReturn('Bob'); } function it_returns_its_new_name_when_it_has_been_renamed() { $this->beConstructedWith('Bob'); $this->changeNameTo('Alice'); $this->getName()->shouldReturn('Alice'); } }
  60. 60. # spec/HelloWorld/PersonSpec.php class PersonSpec extends ObjectBehavior { function let() { $this->beConstructedWith('Bob'); } function it_returns_the_name_it_is_created_with() { $this->getName()->shouldReturn('Bob'); } function it_returns_its_new_name_when_it_has_been_renamed() { $this->changeNameTo('Alice'); $this->getName()->shouldReturn('Alice'); } }
  61. 61. # src/HelloWorld/Person.php class Person { private $name; // … public function changeNameTo($argument1) { // TODO: write logic here } }
  62. 62. # src/HelloWorld/Person.php class Person { private $name; // … public function changeNameTo($name) { $this->name = $name; } }
  63. 63. Describing collaboration - Stubs Stubs are when we describe how we interact with objects we query 4 willReturn() 4 Doesn't care when or how many times the method is called
  64. 64. Describing collaboration - Mocking and Spying Mocks or Spies are when we describe how we interact with objects we command 4 shouldBeCalled() or shouldHaveBeenCalled() 4 Verifies that the method is called
  65. 65. Final example for Greeter: When it greets Bob, the message "Hello Bob" should be logged
  66. 66. # spec/HelloWorld/GreeterSpec.php class GreeterSpec extends ObjectBehavior { // ... function it_greets_named_things_by_name(Named $named) { $named->getName()->willReturn('Bob'); $this->greet($named)->shouldReturn('Hello, Bob'); } }
  67. 67. # spec/HelloWorld/GreeterSpec.php class GreeterSpec extends ObjectBehavior { function let(Named $named) { $named->getName()->willReturn('Bob'); } // ... function it_greets_named_things_by_name(Named $named) { $this->greet($named)->shouldReturn('Hello, Bob'); } }
  68. 68. # spec/HelloWorld/GreeterSpec.php class GreeterSpec extends ObjectBehavior { function let(Named $named, Logger $logger) { $this->beConstructedWith($logger); $named->getName()->willReturn('Bob'); } // ... function it_logs_the_greetings(Named $named, Logger $logger) { $this->greet($named); $logger->log('Hello, Bob')->shouldHaveBeenCalled(); } }
  69. 69. # src/HelloWorld/Greeter.php class Greeter { public function __construct($argument1) { // TODO: write logic here } public function greet(Named $named = null) { $greeting = 'Hello'; if ($named) { $greeting .= ', ' . $named->getName(); } return $greeting; } }
  70. 70. # src/HelloWorld/Greeter.php class Greeter { private $logger; public function __construct(Logger $logger) { $this->logger = $logger; } public function greet(Named $named = null) { $greeting = 'Hello'; if ($named) { $greeting .= ', ' . $named->getName(); } $this->logger->log($greeting); return $greeting; } }
  71. 71. What have we built?
  72. 72. The domain model
  73. 73. Kata - String Calculator 4 The String Calculator has more than one responsibility: 1. Splitting the string into components 2. Combining them together again by summing 4 Do the exercise again, but this time use more than one object to achieve the task
  74. 74. An high level test echo $result = (new Calculator(new Splitter(), new Parser()))->evaluate('[x]1x2x3'); 4 When your application becomes composed of small self-contained objects, you need some higher level of testing (e.g. PHPUnit or Behat)
  75. 75. Kata - string calculator 4 Empty string should evaluate to zero 4 Zero as a string should evaluate to zero 4 Numeric string should evaluate to that number 4 Space separated numbers should be added together 4 Whitespace separated numbers should be added together 4 Custom separator can be specified (e.g. ’[+]1+2+3’ -> 6)
  76. 76. Thank you! 4 @ciaranmcnulty 4 Lead Maintainer of PhpSpec 4 SeniorTrainer at: Inviqa / Sensio Labs UK / Session Digital / iKOS 4 https://joind.in/talk/view/15424 Questions?

×