tdd with phpspec

Post on 15-Apr-2017

1.479 Views

Category:

Technology

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

TDD with PhpSpecwith Ciaran McNulty at

PHPNW 2015

TDD vs BDD(or are they the same?)

BDD is a second-generation, outside-

in, pull-based, multiple-

stakeholder…1

Dan North

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

1Dan North

BDD is the art of using examples in conversation to

illustrate behaviour1

Liz Keogh

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

Behaviour Driven Development4 Before you write your

code, describe how it should behave using examples

4 Then, Implement the behaviour you have described

SpecBDD with PhpSpec

Describing individual classes

History1.0 - Inspired by RSpec

4 Pádraic Brady and Travis Swicegood

History2.0beta - Inspired by 1.0

4 Marcello Duarte and Konstantin Kudryashov (Everzet)

4 Ground-up rewrite

4 No BC in specs

Design principles4 Optimise for descriptiveness

4 Encourage good design

4 Encourage TDD cycle

4 Do it the PHP way

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

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

A requirement:

We need a component that greets people

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]

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

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

Usage:phpspec run

# src/HelloWorld/Greeter.php

namespace HelloWorld;

class Greeter{}

An example for Greeter:

When this greets, it should return "Hello"

# spec/HelloWorld/GreeterSpec.php

class GreeterSpec extends ObjectBehavior{ // ...

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

# src/HelloWorld/Greeter.php

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

So now I write some code?

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

# src/PhpDay/HelloWorld/Greeter.php

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

Describing valuesMatchers

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

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

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

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

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

Describing Exceptions

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

or

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

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

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

The TDD Workflow

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.

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”

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

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

Getting used to TDD

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

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

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

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)

Describing Collaboration

Another example for Greeter:

When this greets a person called "Bob",

it should return "Hello, Bob"

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

The Interface Segregation Principle:

No client should be forced to depend on

methods it does not use1

Robert C Martin

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

# src/HelloWorld/Named.php

namespace HelloWorld;

interface Named{ public function getName();}

# src/HelloWorld/Greeter.php

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

Finally now we write some code!

# src/HelloWorld/Greeter.php

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

}

# src/HelloWorld/Greeter.php

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

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

return $greeting; }}

An example for a Person:

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

return "Bob"

# spec/HelloWorld/PersonSpec.php

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

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

# src/HelloWorld/Person.php

class Person{

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

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

# src/HelloWorld/Person.php

class Person implements Named{ private $name;

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

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

Another example for a Person:

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

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

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

# src/HelloWorld/Person.php

class Person{ private $name;

// …

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

# src/HelloWorld/Person.php

class Person{ private $name;

// …

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

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

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

Final example for Greeter:

When it greets Bob, the message "Hello

Bob" should be logged

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

}

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

}

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

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

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

What have we built?

The domain model

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

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)

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)

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?

top related