TDD with PhpSpec
with 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.
1
Dan North
BDD is the art of
using examples in
conversation to
illustrate behaviour
1
Liz Keogh
Test Driven
Development
4 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
Development
4 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
History
1.0 - Inspired by
RSpec
4 Pádraic Brady and
Travis Swicegood
History
2.0beta - Inspired by 1.0
4 Marcello Duarte and Konstantin
Kudryashov (Everzet)
4 Ground-up rewrite
4 No BC in specs
Design principles
4 Optimise for descriptiveness
4 Encourage good design
4 Encourage TDD cycle
4 Do it the PHP way
History
2.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 behaviour
4 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 specHelloWorld;
use PhpSpecObjectBehavior;
use ProphecyArgument;
class GreeterSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('HelloWorldGreeter');
}
}
Verifying object behaviour
4 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 it
4 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 values
Matchers
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 matchers
function 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));
}
];
}
Wildcarding
4 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();
Exercise
4 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
TDD
by 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
behaviour
4 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
pass
4 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
design
4 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
Pairing
4 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
Kata
4 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
Kata
4 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 calculator
Design 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 HelloWorldPerson;
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 use
1
Robert C Martin
# spec/HelloWorld/GreeterSpec.php
use HelloWorldNamed;
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 -
Stubs
Stubs 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 Spying
Mocks 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 Calculator
4 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 test
echo $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 calculator
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)
Thank you!
4 @ciaranmcnulty
4 Lead Maintainer of PhpSpec
4 SeniorTrainer at:
Inviqa / Sensio Labs UK / Session Digital / iKOS
4 https://joind.in/talk/view/15424
Questions?

TDD with PhpSpec