Slide 1

Slide 1 text

Is TDD dead? a cheeky guide to phpspec 12 August 2015

Slide 2

Slide 2 text

Marek Matulka Software Engineer SensioLabs UK @super_marek

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

no tests

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

my first unit tests

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

discovering BDD

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Write a failing scenario (feature)

Slide 11

Slide 11 text

Write a failing scenario (feature) Write a failing test

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Write a failing scenario (feature) Write a failing test Make your test pass Refactor your code

Slide 14

Slide 14 text

Write a failing scenario (feature) Write a failing test Make your test pass Refactor your code Repeat the fail-pass-refactor cycle as necessary

Slide 15

Slide 15 text

Write a failing scenario (feature) Write a failing test Make your test pass Refactor your code Make your feature pass

Slide 16

Slide 16 text

Write a failing scenario (feature) Write a failing test Make your test pass Refactor your code Make your feature pass BDD TDD

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

Existing project $ composer require --dev phpspec/phpspec

Slide 20

Slide 20 text

Starting new project? { "require-dev": { "phpspec/phpspec": "~2.0" }, "config": { "bin-dir": "bin" }, "autoload": {"psr-0": {"": "src"}} }

Slide 21

Slide 21 text

first spec $ bin/phpspec desc Acme\\StringCalculator

Slide 22

Slide 22 text

first spec $ bin/phpspec desc Acme\\StringCalculator Specification for Acme\StringCalculator created in /home/marek/Workspace/phpsw/spec/Acme/StringCalculatorSpec.php. $

Slide 23

Slide 23 text

first spec $ bin/phpspec desc Acme\\StringCalculator Specification for Acme\StringCalculator created in /home/marek/Workspace/phpsw/spec/Acme/StringCalculatorSpec.php. $ bin/phpspec run

Slide 24

Slide 24 text

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]

Slide 25

Slide 25 text

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 $

Slide 26

Slide 26 text

spec/Acme/StringCalculatorSpec.php shouldHaveType('Acme\StringCalculator'); } }

Slide 27

Slide 27 text

src/Acme/StringCalculator.php

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

spec/Acme/StringCalculatorSpec.php class StringCalculatorSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Acme\StringCalculator'); } function it_calculates_empty_string_and_returns_zero() { $this->calculate('')->shouldReturn(0); } }

Slide 30

Slide 30 text

$ bin/phpspec run Do you want me to create `Acme\StringCalculator::calculate()` for you? [Y/n] Method Acme\StringCalculator::calculate() has been created. $

Slide 31

Slide 31 text

$ 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. $

Slide 32

Slide 32 text

$ 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

Slide 33

Slide 33 text

src/Acme/StringCalculator.php

Slide 34

Slide 34 text

$ 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

Slide 35

Slide 35 text

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); } }

Slide 36

Slide 36 text

stub

Slide 37

Slide 37 text

namespace Acme; interface LearnerRepository { /** * @param integer $id * * @return Learner */ public function findLearnerById($id); }

Slide 38

Slide 38 text

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); } }

Slide 39

Slide 39 text

mock

Slide 40

Slide 40 text

namespace Acme; interface MessageDispatcher { /** * @param integer $id * * @return Message */ public function dispatch(Message $message); }

Slide 41

Slide 41 text

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); } }

Slide 42

Slide 42 text

spy

Slide 43

Slide 43 text

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(); } }

Slide 44

Slide 44 text

constructing objects

Slide 45

Slide 45 text

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... }

Slide 46

Slide 46 text

class LearnerSpec extends ObjectBehaviour { function let() { $this->beConstractedThrough( 'fromEmail', ['[email protected]'] ); } }

Slide 47

Slide 47 text

class LearnerSpec extends ObjectBehaviour { function let() { $this->beConstractedThrough( 'fromEmail', ['[email protected]'] ); } function it_can_be_created_with_name() { $this->beConstractedThrough('fromName', ['Test User']); $this->getEmail()->shouldBe(NULL); $this->getName()->shouldReturn('Test User'); }

Slide 48

Slide 48 text

matchers

Slide 49

Slide 49 text

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"); }

Slide 50

Slide 50 text

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(''); } }

Slide 51

Slide 51 text

Throw Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_not_allow_empty_name() { $this->beConstructedWith(''); $this->shouldThrow('\InvalidArgumentException') ->duringInstantiation(); } }

Slide 52

Slide 52 text

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); } }

Slide 53

Slide 53 text

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'); } }

Slide 54

Slide 54 text

Count Matcher class TrainingAdministratorSpec extends ObjectBehavior { function it_should_have_one_role() { $this->getRoles()->shouldHaveCount(1); } }

Slide 55

Slide 55 text

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'); } }

Slide 56

Slide 56 text

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'); }

Slide 57

Slide 57 text

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); }, ]; }

Slide 58

Slide 58 text

why phpspec?

Slide 59

Slide 59 text

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!

Slide 60

Slide 60 text

when not to use phpspec? - functional tests - use behat or phpunit - integration tests - use phpunit

Slide 61

Slide 61 text

how does it fit hexagonal arch? UI Adapter Log Adapter Data Storage Adapter External Data Adapter Application Domain

Slide 62

Slide 62 text

how does it fit hexagonal arch? UI Adapter Log Adapter Data Storage Adapter External Data Adapter Application Domain

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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!

Slide 65

Slide 65 text

Do test your infrastructure Write integration tests for your repositories and adapters. Always hide infrastructure / external services behind adapters.

Slide 66

Slide 66 text

Want to read more about phpspec? - github.com/phpspec/phpspec - phpspec.net - groups.google.com/forum/#!forum/phpspec-dev - twitter.com/phpsec

Slide 67

Slide 67 text

Questions?

Slide 68

Slide 68 text

Slides: speakerdeck.com/super_marek Give feedback: joind.in/14901 Connect: @super_marek Thank you!