Is TDD dead? A cheeky guide to PhpSpec

Is TDD dead? A cheeky guide to PhpSpec

A look at PhpSpec and why it could be an alternative to PHP Unit.

2bd48651cd01e0ca2e0a255a63da77aa?s=128

Marek Matulka

August 12, 2015
Tweet

Transcript

  1. 3.
  2. 5.
  3. 7.
  4. 9.
  5. 13.
  6. 14.

    Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Repeat the fail-pass-refactor cycle as necessary
  7. 15.

    Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Make your feature pass
  8. 16.

    Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Make your feature pass BDD TDD
  9. 17.

    Write a failing scenario (feature) Write a failing test Make

    your test pass Refactor your code Make your feature pass BDD TDD External Quality Internal Quality
  10. 18.
  11. 20.

    Starting new project? { "require-dev": { "phpspec/phpspec": "~2.0" }, "config":

    { "bin-dir": "bin" }, "autoload": {"psr-0": {"": "src"}} }
  12. 22.

    first spec $ bin/phpspec desc Acme\\StringCalculator Specification for Acme\StringCalculator created

    in /home/marek/Workspace/phpsw/spec/Acme/StringCalculatorSpec.php. $
  13. 23.

    first spec $ bin/phpspec desc Acme\\StringCalculator Specification for Acme\StringCalculator created

    in /home/marek/Workspace/phpsw/spec/Acme/StringCalculatorSpec.php. $ bin/phpspec run
  14. 24.

    first spec $ bin/phpspec run Acme/StringCalculator 10 - it is

    initializable class Acme\StringCalculator does not exist. Do you want me to create `Acme\StringCalculator` for you? [Y/n]
  15. 25.

    first spec Do you want me to create `Acme\StringCalculator` for

    you? [Y/n] Class Acme\StringCalculator created in /home/marek/Workspace/phpsw/src/Acme/StringCalculator.php. 1 specs 1 example (1 passed) 9ms $
  16. 26.

    spec/Acme/StringCalculatorSpec.php <?php namespace spec\Acme; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class StringCalculatorSpec

    extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Acme\StringCalculator'); } }
  17. 28.
  18. 30.

    $ bin/phpspec run Do you want me to create `Acme\StringCalculator::calculate()`

    for you? [Y/n] Method Acme\StringCalculator::calculate() has been created. $
  19. 31.

    $ bin/phpspec run --fake Do you want me to create

    `Acme\StringCalculator::calculate()` for you? [Y/n] Method Acme\StringCalculator::calculate() has been created. Do you want me to make `Acme\StringCalculator::calculate()` always return 0 for you? [Y/n] Method Acme\StringCalculator::calculate() has been modified. $
  20. 32.

    $ bin/phpspec run --format pretty Acme\StringCalculator 10 ✔ is initializable

    15 ✔ calculates empty string and returns zero 1 specs 2 examples (2 passed) 6ms $ bin/phpspec run -fpretty
  21. 34.

    $ bin/phpspec run --format pretty Acme\StringCalculator 10 ✔ is initializable

    15 ✔ calculates empty string and returns zero 20 ✔ adds two plus separated integers 25 ✔ adds many plus separated integers 1 specs 4 examples (4 passed) 8ms $ TDD
  22. 35.

    class StringCalculator { /** * @param string $input * *

    @return integer */ public function calculate($input) { $parts = explode('+', $input); array_walk($parts, function (&$item) { return trim($item); }); return array_sum($parts); } }
  23. 36.
  24. 37.

    namespace Acme; interface LearnerRepository { /** * @param integer $id

    * * @return Learner */ public function findLearnerById($id); }
  25. 38.

    use Acme\LearnerRepository; class LearnerDetailsControllerSpec extends ObjectBehaviour { function it_loads_learner(LearnerRepository $repository)

    { $learner = new Learner(); $repository->findLearnerById(5)->willReturn($learner); $this->learnerDetailsAction(5)->shouldReturn($learner); } }
  26. 39.
  27. 40.

    namespace Acme; interface MessageDispatcher { /** * @param integer $id

    * * @return Message */ public function dispatch(Message $message); }
  28. 41.

    use Acme\LearnerRepository; use Acme\MessageDispatcher; class LearnerDetailsControllerSpec extends ObjectBehaviour { function

    it_loads_learner(LearnerRepository $repository, MessageDispatcher $dispatcher ) { $learner = new Learner(); $repository->findLearnerById(5)->willReturn($learner); $dispatcher->dispatch(Argument::type(Message::class) ->shouldBeCalled(); $this->learnerDetailsAction(5)->shouldReturn($learner); } }
  29. 42.

    spy

  30. 43.

    use Acme\LearnerRepository; use Acme\MessageDispatcher; class LearnerDetailsControllerSpec extends ObjectBehaviour { function

    it_loads_learner(LearnerRepository $repository, MessageDispatcher $dispatcher ) { $learner = new Learner(); $repository->findLearnerById(5)->willReturn($learner); $this->learnerDetailsAction(5)->shouldReturn($learner); $dispatcher->dispatch(Argument::type(Message::class) ->shouldHaveBeenCalled(); } }
  31. 45.

    use Acme\LearnerRepository; use Acme\MessageDispatcher; class LearnerDetailsControllerSpec extends ObjectBehaviour { function

    let( LearnerRepository $repository, MessageDispatcher $dispatcher ) { $this->beConstractedWith($repository, $dispatcher); } function it_loads_learner(LearnerRepository $repository, MessageDispatcher $dispatcher ) { // test... }
  32. 47.

    class LearnerSpec extends ObjectBehaviour { function let() { $this->beConstractedThrough( 'fromEmail',

    ['user@example.com'] ); } function it_can_be_created_with_name() { $this->beConstractedThrough('fromName', ['Test User']); $this->getEmail()->shouldBe(NULL); $this->getName()->shouldReturn('Test User'); }
  33. 48.
  34. 49.

    Identity Matcher class TrainingAdministratorSpec extends ObjectBehavior { function let() {

    $this->beConstructedWith("Test User", "ROLE_COORDINATOR"); } function it_is_a_training_coordinator() { $this->isManager()->shouldBe(false); $this->isCoordinator()->shouldBe(true); $this->getRole()->shouldReturn("ROLE_COORDINATOR"); $this->getName()->shouldBeEqualTo("Test User"); }
  35. 50.

    Throw Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_not_allow_empty_name() {

    $this->shouldThrow('\InvalidArgumentException') ->during('changeName', ['']); } function it_should_not_allow_empty_name() { $this->shouldThrow(new \InvalidArgumentException()) ->duringChangeName(''); } }
  36. 51.

    Throw Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_not_allow_empty_name() {

    $this->beConstructedWith(''); $this->shouldThrow('\InvalidArgumentException') ->duringInstantiation(); } }
  37. 52.

    Type Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_be_a_training_administrator() {

    $this->shouldHaveType(TrainingAdministrator::class); $this->shouldReturnAnInstanceOf(TrainingAdministrator::class); $this->shouldBeAnInstanceOf(TrainingAdministrator::class); $this->shouldImplement(TrainingAdministrator::class); } }
  38. 53.

    Object State Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_be_a_training_administrator()

    { $this->isManager()->shouldBe(false); $this->isCoordinator()->shouldBe(true); // calls TrainingAdministrator::isCoordinator() $this->shoudBeCoordinator(); // calls TrainingAdministrator::hasRole() $this->shouldHaveRole('ROLE_COORDINATOR'); } }
  39. 55.

    String Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_have_a_string_as_name() {

    $this->beConstructedWith('Test User'); $this->getName()->shouldBeString(); $this->getName()->shouldStartWith('Test'); $this->getName()->shouldEndWith('User'); $this->getName()->shouldMatch('/test/i'); } }
  40. 56.

    Array Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_have_an_array_as_roles() {

    $this->getRoles()->shouldBeArray(); $this->getRoles()->shouldContain('ROLE_COORDINATOR'); } function it_should_expose_display_data_bag() { $this->toDisplay()->shouldHaveKeyWithValue('name', 'Test User'); $this->toDisplay()->shouldHaveKey('name'); }
  41. 57.

    Inline Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_expose_display_data_bag() {

    $this->toDisplay()->shouldHaveKey('name'); } function getMatchers() { return [ 'haveKey' => function ($subject, $key) { return array_key_exists($key, $subject); }, ]; }
  42. 59.

    why phpspec? - easy to write specs before code -

    does (some) code generation for you - helps to stay in the red-green-refactor loop - adds code level documentation - fun to use!
  43. 60.

    when not to use phpspec? - functional tests - use

    behat or phpunit - integration tests - use phpunit
  44. 61.

    how does it fit hexagonal arch? UI Adapter Log Adapter

    Data Storage Adapter External Data Adapter Application Domain
  45. 62.

    how does it fit hexagonal arch? UI Adapter Log Adapter

    Data Storage Adapter External Data Adapter Application Domain
  46. 63.

    What to spec? - simple answer: everything! - longer answer:

    - everything you can - but don’t use PhpSpec for integration/functional tests - leave it to Behat and/or PHP Unit
  47. 64.

    Use fakes for functional tests Fakes are simplified implementations of

    your infrastructure / external adapters. - e.g. InMemoryRepository will be a lot faster than Doctrine with mysql when run from inside VM!
  48. 65.

    Do test your infrastructure Write integration tests for your repositories

    and adapters. Always hide infrastructure / external services behind adapters.
  49. 66.

    Want to read more about phpspec? - github.com/phpspec/phpspec - phpspec.net

    - groups.google.com/forum/#!forum/phpspec-dev - twitter.com/phpsec