tdd with phpspec

109
TDD with PhpSpec with Ciaran McNulty a t PHPNW 2015

Upload: ciaranmcnulty

Post on 15-Apr-2017

1.479 views

Category:

Technology


0 download

TRANSCRIPT

Page 1: TDD with PhpSpec

TDD with PhpSpecwith Ciaran McNulty at

PHPNW 2015

Page 2: TDD with PhpSpec

TDD vs BDD(or are they the same?)

Page 3: TDD with PhpSpec

BDD is a second-generation, outside-

in, pull-based, multiple-

stakeholder…1

Dan North

Page 4: TDD with PhpSpec

…multiple-scale, high-automation, agile methodology.

1Dan North

Page 5: TDD with PhpSpec

BDD is the art of using examples in conversation to

illustrate behaviour1

Liz Keogh

Page 6: TDD with PhpSpec

Test Driven Development4 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

Page 7: TDD with PhpSpec

Behaviour Driven Development4 Before you write your

code, describe how it should behave using examples

4 Then, Implement the behaviour you have described

Page 8: TDD with PhpSpec
Page 9: TDD with PhpSpec
Page 10: TDD with PhpSpec
Page 11: TDD with PhpSpec
Page 12: TDD with PhpSpec

SpecBDD with PhpSpec

Describing individual classes

Page 13: TDD with PhpSpec

History1.0 - Inspired by RSpec

4 Pádraic Brady and Travis Swicegood

Page 14: TDD with PhpSpec

History2.0beta - Inspired by 1.0

4 Marcello Duarte and Konstantin Kudryashov (Everzet)

4 Ground-up rewrite

4 No BC in specs

Page 15: TDD with PhpSpec

Design principles4 Optimise for descriptiveness

4 Encourage good design

4 Encourage TDD cycle

4 Do it the PHP way

Page 16: TDD with PhpSpec

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

Page 17: TDD with PhpSpec

Installation via Composer{ "require-dev": { "phpspec/phpspec": "~2.0" }, "config": { "bin-dir": "bin" }, "autoload": {"psr-0": {"": "src"}}}

Page 18: TDD with PhpSpec
Page 19: TDD with PhpSpec

A requirement:

We need a component that greets people

Page 20: TDD with PhpSpec

Describing object behaviour4 We describe an object using a

Specification

4 A specification is made up of Examples illustrating different scenarios

Usage:phpspec describe [Class]

Page 21: TDD with PhpSpec
Page 22: TDD with PhpSpec

# spec/HelloWorld/GreeterSpec.php

namespace spec\HelloWorld;

use PhpSpec\ObjectBehavior;use Prophecy\Argument;

class GreeterSpec extends ObjectBehavior{ function it_is_initializable() { $this->shouldHaveType('HelloWorld\Greeter'); }}

Page 23: TDD with PhpSpec

Verifying object behaviour4 Compare the real objects' behaviours with the examples

Usage:phpspec run

Page 24: TDD with PhpSpec
Page 25: TDD with PhpSpec
Page 26: TDD with PhpSpec

# src/HelloWorld/Greeter.php

namespace HelloWorld;

class Greeter{}

Page 27: TDD with PhpSpec

An example for Greeter:

When this greets, it should return "Hello"

Page 28: TDD with PhpSpec

# spec/HelloWorld/GreeterSpec.php

class GreeterSpec extends ObjectBehavior{ // ...

function it_greets_by_saying_hello() { $this->greet()->shouldReturn('Hello'); }}

Page 29: TDD with PhpSpec
Page 30: TDD with PhpSpec
Page 31: TDD with PhpSpec

# src/HelloWorld/Greeter.php

class Greeter{ public function greet() { // TODO: write logic here }}

Page 32: TDD with PhpSpec

So now I write some code?

Page 33: TDD with PhpSpec

Fake it till you make it4 Do the simplest thing that works

4 Only add complexity later when more examples drive it

phpspec run --fake

Page 34: TDD with PhpSpec
Page 35: TDD with PhpSpec
Page 36: TDD with PhpSpec

# src/PhpDay/HelloWorld/Greeter.php

class Greeter{ public function greet() { return 'Hello'; }}

Page 37: TDD with PhpSpec

Describing valuesMatchers

Page 38: TDD with PhpSpec

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

Page 39: TDD with PhpSpec

Object state// isAdmin() should return true$this->getUser()->shouldBeAdmin();

// hasLoggedInUser() should return true$this->shouldHaveLoggedInUser();

Page 40: TDD with PhpSpec

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

Page 41: TDD with PhpSpec

Wildcarding4 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...;

Page 42: TDD with PhpSpec

Describing Exceptions

Page 43: TDD with PhpSpec

The shouldThrow matcher$this->shouldThrow(\InvalidArgumentException::class) ->duringSave($user);

or

$this->shouldThrow(\InvalidArgumentException::class) ->during(‘save’, [$user]);

Page 44: TDD with PhpSpec

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

Page 45: TDD with PhpSpec

Exercise4 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

Page 46: TDD with PhpSpec

The TDD Workflow

Page 47: TDD with PhpSpec

The Rules of TDDby 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.

Page 48: TDD with PhpSpec

Test - Describe the next behaviour4 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”

Page 49: TDD with PhpSpec

Code - Make it pass4 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

Page 50: TDD with PhpSpec

Refactor - Improve the design4 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

Page 51: TDD with PhpSpec

Getting used to TDD

Page 52: TDD with PhpSpec

Pairing4 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

Page 53: TDD with PhpSpec

Kata4 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

Page 54: TDD with PhpSpec

Kata4 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

Page 55: TDD with PhpSpec

Kata - string calculatorDesign 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)

Page 56: TDD with PhpSpec

Describing Collaboration

Page 57: TDD with PhpSpec

Another example for Greeter:

When this greets a person called "Bob",

it should return "Hello, Bob"

Page 58: TDD with PhpSpec

# spec/HelloWorld/GreeterSpec.php

use HelloWorld\Person;

class GreeterSpec extends ObjectBehavior{ // ...

function it_greets_a_person_by_name(Person $person) { $person->getName()->willReturn('Bob');

$this->greet($person)->shouldReturn('Hello, Bob'); }}

Page 59: TDD with PhpSpec
Page 60: TDD with PhpSpec

The Interface Segregation Principle:

No client should be forced to depend on

methods it does not use1

Robert C Martin

Page 61: TDD with PhpSpec
Page 62: TDD with PhpSpec

# spec/HelloWorld/GreeterSpec.php

use HelloWorld\Named;

class GreeterSpec extends ObjectBehavior{ // ...

function it_greets_named_things_by_name(Named $named) { $named->getName()->willReturn('Bob');

$this->greet($named)->shouldReturn('Hello, Bob'); }}

Page 63: TDD with PhpSpec
Page 64: TDD with PhpSpec
Page 65: TDD with PhpSpec
Page 66: TDD with PhpSpec

# src/HelloWorld/Named.php

namespace HelloWorld;

interface Named{ public function getName();}

Page 67: TDD with PhpSpec

# src/HelloWorld/Greeter.php

class Greeter{ public function greet() { return 'Hello'; }}

Page 68: TDD with PhpSpec

Finally now we write some code!

Page 69: TDD with PhpSpec

# src/HelloWorld/Greeter.php

class Greeter{ public function greet(Named $named = null) { return 'Hello'; }

}

Page 70: TDD with PhpSpec

# src/HelloWorld/Greeter.php

class Greeter{ public function greet(Named $named = null) { $greeting = 'Hello';

if ($named) { $greeting .= ', ' . $named->getName(); }

return $greeting; }}

Page 71: TDD with PhpSpec
Page 72: TDD with PhpSpec

An example for a Person:

When you ask a person named "Bob" for their name, they

return "Bob"

Page 73: TDD with PhpSpec
Page 74: TDD with PhpSpec

# spec/HelloWorld/PersonSpec.php

class PersonSpec extends ObjectBehavior{ function it_returns_the_name_it_is_created_with() { $this->beConstructedWith('Bob');

$this->getName()->shouldReturn('Bob'); }}

Page 75: TDD with PhpSpec
Page 76: TDD with PhpSpec
Page 77: TDD with PhpSpec
Page 78: TDD with PhpSpec
Page 79: TDD with PhpSpec

# src/HelloWorld/Person.php

class Person{

public function __construct($argument1) { // TODO: write logic here }

public function getName() { // TODO: write logic here }}

Page 80: TDD with PhpSpec

# src/HelloWorld/Person.php

class Person implements Named{ private $name;

public function __construct($name) { $this->name = $name; }

public function getName() { return $this->name; }}

Page 81: TDD with PhpSpec
Page 82: TDD with PhpSpec

Another example for a Person:

When a person named "Bob" changes their name to "Alice", when you ask their name they return "Alice"

Page 83: TDD with PhpSpec

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

Page 84: TDD with PhpSpec

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

Page 85: TDD with PhpSpec
Page 86: TDD with PhpSpec
Page 87: TDD with PhpSpec

# src/HelloWorld/Person.php

class Person{ private $name;

// …

public function changeNameTo($argument1) { // TODO: write logic here }}

Page 88: TDD with PhpSpec

# src/HelloWorld/Person.php

class Person{ private $name;

// …

public function changeNameTo($name) { $this->name = $name; }}

Page 89: TDD with PhpSpec
Page 90: TDD with PhpSpec

Describing collaboration - StubsStubs 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

Page 91: TDD with PhpSpec

Describing collaboration - Mocking and SpyingMocks or Spies are when we describe how we interact with objects we command

4 shouldBeCalled() or shouldHaveBeenCalled()

4 Verifies that the method is called

Page 92: TDD with PhpSpec

Final example for Greeter:

When it greets Bob, the message "Hello

Bob" should be logged

Page 93: TDD with PhpSpec

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

}

Page 94: TDD with PhpSpec

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

}

Page 95: TDD with PhpSpec

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

Page 96: TDD with PhpSpec
Page 97: TDD with PhpSpec
Page 98: TDD with PhpSpec
Page 99: TDD with PhpSpec
Page 100: TDD with PhpSpec

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

Page 101: TDD with PhpSpec

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

Page 102: TDD with PhpSpec
Page 103: TDD with PhpSpec

What have we built?

Page 104: TDD with PhpSpec

The domain model

Page 105: TDD with PhpSpec
Page 106: TDD with PhpSpec

Kata - String Calculator4 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

Page 107: TDD with PhpSpec

An high level testecho $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)

Page 108: TDD with PhpSpec

Kata - string calculator4 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)

Page 109: TDD with PhpSpec

Thank you!4 @ciaranmcnulty

4 Lead Maintainer of PhpSpec

4 Senior Trainer at:Inviqa / Sensio Labs UK / Session Digital / iKOS

4 https://joind.in/talk/view/15424

Questions?