TDD with PhpSpec
-
Upload
ciaranmcnulty -
Category
Technology
-
view
1.479 -
download
0
Transcript of TDD with PhpSpec
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?