driving design with phpspec
DESCRIPTION
What is BDD? Is it the same as TDD, or something quite different? This talk will answer these questions, and show how PhpSpec can be integrated into your development workflow to drive your Object Oriented design. Plus: a sneak peak at some of the new features in the forthcoming 2.1 release.TRANSCRIPT
Driving Development with PhpSpec
with Ciaran McNulty
PHPLondon November 2014
My experiences4 Unit testing since 2004
4 Test Driven Development since 2005(ish)
4 Behaviour Driven Development since 2012
TDD vs BDD(or are they the same?)
BDD is a second-generation, outside-in,
pull-based, multiple-stakeholder…
1Dan North
…multiple-scale, high-automation, agile
methodology.1
Dan 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 you described
SpecBDD with PhpSpecDescribing 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
History2.0 stable - The boring bits
4 Me
4 Christophe Coevoet
4 Jakub Zalas
4 Richard Miller
4 Gildas Quéméner
Installation via Composer{ "require-dev": { "phpspec/phpspec": "~2.1-RC1" }, "config": { "bin-dir": "bin" }, "autoload": {"psr-0": {"": "src"}}}
A requirement:
We need something that says hello to 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/PhpLondon/HelloWorld/GreeterSpec.php
namespace spec\PhpLondon\HelloWorld;
use PhpSpec\ObjectBehavior;use Prophecy\Argument;
class GreeterSpec extends ObjectBehavior{ function it_is_initializable() { $this->shouldHaveType('PhpLondon\HelloWorld\Greeter'); }}
Verifying object behaviour4 Compare the real objects' behaviours with the
examples
Usage:phpspec run
/src/PhpLondon/HelloWorld/Greeter.phpnamespace PhpLondon\HelloWorld;
class Greeter{}
An example for Greeter:
When this greets, it should return "Hello"
/spec/PhpLondon/HelloWorld/GreeterSpec.php
class GreeterSpec extends ObjectBehavior{ function it_greets_by_saying_hello() { $this->greet()->shouldReturn('Hello'); }}
/src/PhpLondon/HelloWorld/Greeter.phpclass 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/PhpLondon/HelloWorld/Greeter.phpclass Greeter{ public function greet() { return 'Hello'; }}
Describing valuesMatchers
Describing values - Equality$this->greet()->shouldReturn('Hello');
$this->sum(3,3)->shouldEqual(6);
$user = $this->findById(1234);$user->shouldBe($expectedUser);
$this->numberList() ->shouldBeLike(new ArrayObject([1,2,3]));
Describing values - Type$this->address()->shouldHaveType('EmailAddress');
$this->getTime()->shouldReturnAnInstanceOf('DateTime');
$user = $this->findById(1234);$user->shouldBeAnInstanceOf('User');
$this->shouldImplement('Countable');
Describing values - Strings$this->getStory()->shouldStartWith('A long time ago');$this->getStory()->shouldEndWith('happily ever after');
$this->getSlug()->shouldMatch('/^[0-9a-z]+$/');
Describing values - Arrays$this->getNames()->shouldContain('Tom');
$this->getNames()->shouldHaveKey(0);
$this->getNames()->shouldHaveCount(1);
Describing values - object state// calls isAdmin()$this->getUser()->shouldBeAdmin();
// calls hasLoggedInUser()$this->shouldHaveLoggedInUser();
Describing custom valuesfunction 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)); } ];}
Another example for Greeter:
When this greets Bob, it should return "Hello, Bob"
Wait, what is Bob?
Bob is a PersonWhat is a Person?
An example for a Person:
When you ask a person named "Alice" for their name, they return "Alice"
/spec/PhpLondon/HelloWorld/PersonSpec.php
class PersonSpec extends ObjectBehavior{ function it_returns_the_name_it_is_created_with() { $this->beConstructedWith('Alice');
$this->getName()->shouldReturn('Alice'); }}
/src/PhpLondon/HelloWorld/Person.phpclass Person{ public function __construct($argument1) { // TODO: write logic here }
public function getName() { // TODO: write logic here }}
So now I write some code!
/src/PhpLondon/HelloWorld/Person.phpclass Person{ private $name;
public function __construct($name) { $this->name = $name; }
public function getName() { return $this->name; }}
Another example for a Person:
When a person named "Alice" changes their name
to "Bob", when you ask their name they return
"Bob"
/spec/PhpLondon/HelloWorld/PersonSpec.php
class PersonSpec extends ObjectBehavior{ function it_returns_the_name_it_is_created_with() { $this->beConstructedWith('Alice'); $this->getName()->shouldReturn('Alice'); }}
/spec/PhpLondon/HelloWorld/PersonSpec.php
class PersonSpec extends ObjectBehavior{ function let() { $this->beConstructedWith('Alice'); }
function it_returns_the_name_it_is_created_with() { $this->getName()->shouldReturn('Alice'); }}
/spec/PhpLondon/HelloWorld/PersonSpec.php
class PersonSpec extends ObjectBehavior{ function let() { $this->beConstructedWith('Alice'); }
// …
function it_returns_its_new_name_when_the_name_has_been_changed() { $this->changeNameTo('Bob');
$this->getName()->shouldReturn('Bob'); }}
/src/PhpLondon/HelloWorld/Person.phpclass Person{ private $name;
// …
public function changeNameTo($argument1) { // TODO: write logic here }}
/src/PhpLondon/HelloWorld/Person.phpclass Person{ private $name;
// …
public function changeNameTo($name) { $this->name = $name; }}
Another example for Greeter:
When this greets Bob, it should return "Hello, Bob"
Describing collaboration - StubsStubs are used to describe how we interact with objects we query
4 Maybe it is hard to get the real collaborator to return the value we want
4 Maybe using the real collaborator is expensive
/spec/PhpLondon/HelloWorld/GreeterSpec.php
class GreeterSpec extends ObjectBehavior{ //…
function it_greets_people_by_name(Person $bob) { $bob->getName()->willReturn('Bob');
$this->greet($bob)->shouldReturn('Hello, Bob'); }}
/src/PhpLondon/HelloWorld/Greeter.phpclass Greeter{ public function greet() { return 'Hello'; }}
/src/PhpLondon/HelloWorld/Greeter.phpclass Greeter{ public function greet() { $greeting = 'Hello';
return $greeting; }}
/src/PhpLondon/HelloWorld/Greeter.phpclass Greeter{ public function greet(Person $person = null) { $greeting = 'Hello';
if ($person) { $greeting .= ', ' . $person->getName(); }
return $greeting; }}
Final example for Greeter:
When it greets Bob, the message "Hello Bob" should
be logged
What's a log?
Let's not worry yet
/src/PhpLondon/HelloWorld/Logger.phpinterface Logger{ public function log($message);}
Describing collaboration - Mocks and SpiesMocks or Spies are used to describe how we interact with objects we command
4 Maybe the real command is has side effects
4 Maybe using the real collaborator is expensive
/spec/PhpLondon/HelloWorld/GreeterSpec.php
class GreeterSpec extends ObjectBehavior{ //…
function it_greets_people_by_name(Person $bob) { $bob->getName()->willReturn('Bob'); $this->greet($bob)->shouldReturn('Hello, Bob'); }}
/spec/PhpLondon/HelloWorld/GreeterSpec.php
class GreeterSpec extends ObjectBehavior{ function let(Person $bob) { $bob->getName()->willReturn('Bob'); }
//…
function it_greets_people_by_name(Person $bob) { $this->greet($bob)->shouldReturn('Hello, Bob'); }}
/spec/PhpLondon/HelloWorld/GreeterSpec.php
class GreeterSpec extends ObjectBehavior{ function let(Person $bob, Logger $logger) { $this->beConstructedWith($logger); $bob->getName()->willReturn('Bob'); }
//…
function it_logs_the_greetings(Person $bob, Logger $logger) { $this->greet($bob); $logger->log('Hello, Bob')->shouldHaveBeenCalled(); }}
/src/PhpLondon/HelloWorld/Greeter.phpclass Greeter{ public function __construct($argument1) { // TODO: write logic here }
public function greet(Person $person = null) { $greeting = 'Hello'; if ($person) { $greeting .= ', ' . $person->getName(); }
return $greeting; }}
/src/PhpLondon/HelloWorld/Greeter.phpclass Greeter{ private $logger;
public function __construct(Logger $logger) { $this->logger = $logger; }
public function greet(Person $person = null) { $greeting = 'Hello'; if ($person) { $greeting .= ', ' . $person->getName(); }
$this->logger->log($greeting);
return $greeting; }}
What have we built?
The domain model
Specs as documentation
PhpSpec4 Focuses on being descriptive
4 Makes common dev activities easier or automated
4 Drives your design
2.1 release - soon!4 Rerun after failure
4 --fake option
4 Named constructors: User::named('Bob')
4 PSR-4 support (+ other autoloaders)
4 + lots of small improvements
Me4 Senior Trainer at Inviqa / Sensio Labs UK / Session
Digital
4 Contributor to PhpSpec
4 @ciaranmcnulty
4 https://github.com/ciaranmcnulty/phplondon-phpspec-talk
Questions?