SlideShare a Scribd company logo
Driving Design through Examples
Ciaran McNulty at Dutch PHP Conference 2016
Modelling
by Example
Combining BDD and
DDD concepts
Behaviour
Driven
Development
BDD helps with
1. Building things well
2. Building the right things
3. Building things for the right reason
... we will focus on 1 & 2
BDD is the art of
using examples in
conversations to
illustrate behaviour
— Liz Keogh
Why
Examples?
Requirements as Rules
We are starting a new budget airline flying
between London and Manchester
→ Travellers can collect 1 point for every
£1 they spend on flights
→ 100 points can be redeemed for £10 off
a future flight
→ Flights are taxed at 20%
Rules are
Ambiguous
Ambiguity
→ When spending points do I still earn
new points?
→ Can I redeem more than 100 points on
one flight?
→ Is tax based on the discounted fare or
the original price of the fare?
Examples are
Unambiguous
Examples
If a flight from London to Manchester costs £50:
→ If you pay cash it will cost £50 + £10 tax, and
you will earn 50 new points
→ If you pay entirely with points it will cost 500
points + £10 tax and you will earn 0 new
points
→ If you pay with 100 points it will cost 100
points + £40 + £10 tax and you will earn 0
new points
Examples
are
Objectively
Testable
Gherkin
A formal language
for examples
You do not
have to use
Gherkin
Feature: Earning and spending points on flights
Rules:
- Travellers can collect 1 point for every £1 they spend on flights
- 100 points can be redeemed for £10 off a future flight
Scenario: Earning points when paying cash
Given ...
Scenario: Redeeming points for a discount on a flight
Given ...
Scenario: Paying for a flight entirely using points
Given ...
Gherkin steps
→ Given sets up context for a behaviour
→ When specifies some action
→ Then specifies some outcome
Action + Outcome = Behaviour
Scenario: Earning points when paying cash
Given a flight costs £50
When I pay with cash
Then I should pay £50 for the flight
And I should pay £10 tax
And I should get 50 points
Scenario: Redeeming points for a discount on a flight
Given a flight costs £50
When I pay with cash plus 100 points
Then I should pay £40 for the flight
And I should pay £10 tax
And I should pay 100 points
Scenario: Paying for a flight entirely using points
Given a flight costs £50
When I pay with points only
Then I should pay £0 for the flight
And I should pay £10 tax
And I should pay 500 points
Who writes examples?
Business expert
Testing expert
Development expert
All discussing the feature together
When to write scenarios
→ Before you start work on the feature
→ Not too long before!
→ Whenever you have access to the right
people
Refining scenarios
→ When would this outcome not be true?
→ What other outcomes are there?
→ But what would happen if...?
→ Does this implementation detail
matter?
Scenarios
are not
Contracts
Scenarios
→ Create a shared understanding of a
feature
→ Give a starting definition of done
→ Provide an objective indication of how
to test a feature
Domain
Driven
Design
DDD tackles
complexity by focusing
the team's attention
on knowledge of the
domain
— Eric Evans
Invest time in
understanding
the business
Ubiquitous Language
→ A shared way of speaking about
domain concepts
→ Reduces the cost of translation when
business and development
communicate
→ Try to establish and use terms the
business will understand
Modelling
by Example
By embedding
Ubiquitous Language in
your scenarios, your
scenarios naturally
become your domain
model
— Konstantin Kudryashov (@everzet)
Principles
→ The best way to understand the
domain is by discussing examples
→ Write scenarios that capture ubiquitous
language
→ Write scenarios that illustrate real
situations
→ Directly drive the code model from
those examples
Directly
driving
code with
Behat?
Layered architecture
UI testing with Behat
Testing through the UI
→ Slow to execute
→ Brittle
→ Makes you design the domain and UI at
the same time
Test the domain first
Testing with real infrastructure
→ Slow to execute
→ Brittle
→ Makes you design the domain and
infrastructure at the same time
Test with fake infrastructure first
Improving
scenarios
Scenario: Earning points when paying cash
Given a flight costs £50
When I pay with cash
Then I should pay £50 for the flight
And I should pay £10 tax
And I should get 50 points
Scenario: Redeeming points for a discount on a flight
Given a flight costs £50
When I pay with cash plus 100 points
Then I should pay £40 for the flight
And I should pay £10 tax
And I should pay 100 points
Scenario: Paying for a flight entirely using points
Given a flight costs £50
When I pay with points only
Then I should pay £0 for the flight
And I should pay £10 tax
And I should pay 500 points
Add
realistic
details
Background:
Given a flight from "London" to "Manchester" costs £50
Scenario: Earning points when paying cash
When I fly from "London" to "Manchester"
And I pay with cash
Then I should pay £50 for the flight
And I should pay £10 tax
And I should get 50 points
Actively
seek terms
from the
domain
→ What words do you use to talk about
these things?
→ Points? Paying? Cash Fly?
→ Is the cost really attached to a flight?
→ Do you call this thing "tax"?
→ How do you think about these things?
Get good
at listening
Lessons from the conversation
→ Price belongs to a Fare for a specific Route
→ Flight is independently assigned to a
Route
→ Some sort of fare listing system controls
Fares
→ I get quoted a cost at the point I purchase
a ticket
This is really useful to know!
Background:
Given a flight "XX-100" flies the "LHR" to "MAN" route
And the current listed fare for the "LHR" to "MAN" route is £50
Scenario: Earning points when paying cash
When I am issued a ticket on flight "XX-100"
And I pay £50 cash for the ticket
Then the ticket should be completely paid
And the ticket should be worth 50 loyalty points
Driving the
domain
model with
Behat
Configure a Behat suite
default:
suites:
core:
contexts: [ FlightsContext ]
Create a context
class FlightsContext implements Context
{
/**
* @Given a flight :arg1 flies the :arg2 to :arg3 route
*/
public function aFlightFliesTheRoute($arg1, $arg2, $arg3)
{
throw new PendingException();
}
// ...
}
Run Behat
Model
values as
Value
Objects
class FlightsContext implements Context
{
/**
* @Given a flight :flightnumber flies the :origin to :destination route
*/
public function aFlightFliesTheRoute($flightnumber, $origin, $destination)
{
$this->flight = new Flight(
FlightNumber::fromString($flightnumber),
Route::between(
Airport::fromCode($origin),
Airport::fromCode($destination)
)
);
}
// ...
}
Transformations
/**
* @Transform :flightnumber
*/
public function transformFlightNumber($number)
{
return FlightNumber::fromString($number);
}
/**
* @Transform :origin
* @Transform :destination
*/
public function transformAirport($code)
{
return Airport::fromCode($code);
}
/**
* @Given a flight :flightnumber flies the :origin to :destination route
*/
public function aFlightFliesTheRoute(
FlightNumber $flightnumber,
Airport $origin,
Airport $destination
)
{
$this->flight = new Flight(
$flightnumber, Route::between($origin, $destination)
);
}
> vendor/bin/behat
PHP Fatal error: Class 'Flight' not found
Describe
objects
with
PhpSpec
class AirportSpec extends ObjectBehavior
{
function it_can_be_represented_as_a_string()
{
$this->beConstructedFromCode('LHR');
$this->asCode()->shouldReturn('LHR');
}
function it_cannot_be_created_with_invalid_code()
{
$this->beConstructedFromCode('1234566XXX');
$this->shouldThrow(Exception::class)->duringInstantiation();
}
}
class Airport
{
private $code;
private function __construct($code)
{
if (!preg_match('/^[A-Z]{3}$/', $code)) {
throw new InvalidArgumentException('Code is not valid');
}
$this->code = $code;
}
public static function fromCode($code)
{
return new Airport($code);
}
public function asCode()
{
return $this->code;
}
}
Driving Design through Examples
/**
* @Given the current listed fare for the :arg1 to :arg2 route is £:arg3
*/
public function theCurrentListedFareForTheToRouteIsPs($arg1, $arg2, $arg3)
{
throw new PendingException();
}
Model boundaries
with Interfaces
interface FareList
{
public function listFare(Route $route, Fare $fare);
}
Create in-memory versions for
testing
namespace Fake;
class FareList implements FareList
{
private $fares = [];
public function listFare(Route $route, Fare $fare)
{
$this->fares[$route->asString()] = $fare;
}
}
/**
* @Given the current listed fare for the :origin to :destination route is £:fare
*/
public function theCurrentListedFareForTheToRouteIsPs(
Airport $origin,
Airport $destination,
Fare $fare
)
{
$this->fareList = new FakeFareList();
$this->fareList->listFare(
Route::between($origin, $destination),
Fare::fromString($fare)
);
}
Run Behat
/**
* @When Iam issued a ticket on flight :arg1
*/
public function iAmIssuedATicketOnFlight($arg1)
{
throw new PendingException();
}
/**
* @When I am issued a ticket on flight :flight
*/
public function iAmIssuedATicketOnFlight()
{
$ticketIssuer = new TicketIssuer($this->fareList);
$this->ticket = $ticketIssuer->issueOn($this->flight);
}
> vendor/bin/behat
PHP Fatal error: Class 'TicketIssuer' not found
class TicketIssuerSpec extends ObjectBehavior
{
function it_can_issue_a_ticket_for_a_flight(Flight $flight)
{
$this->issueOn($flight)->shouldHaveType(Ticket::class);
}
}
class TicketIssuer
{
public function issueOn(Flight $flight)
{
return Ticket::costing(Fare::fromString('10000.00'));
}
}
Run Behat
/**
* @When I pay £:fare cash for the ticket
*/
public function iPayPsCashForTheTicket(Fare $fare)
{
$this->ticket->pay($fare);
}
PHP Fatal error: Call to undefined method Ticket::pay()
class TicketSpec extends ObjectBehavior
{
function it_can_be_paid()
{
$this->pay(Fare::fromString("10.00"));
}
}
class Ticket
{
public function pay(Fare $fare)
{
}
}
Run Behat
The model will be
anaemicUntil you get to Then
/**
* @Then the ticket should be completely paid
*/
public function theTicketShouldBeCompletelyPaid()
{
throw new PendingException();
}
/**
* @Then the ticket should be completely paid
*/
public function theTicketShouldBeCompletelyPaid()
{
assert($this->ticket->isCompletelyPaid() == true);
}
PHP Fatal error: Call to undefined method Ticket::isCompletelyPaid()
class TicketSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedCosting(Fare::fromString("50.00"));
}
function it_is_not_completely_paid_initially()
{
$this->shouldNotBeCompletelyPaid();
}
function it_can_be_paid_completely()
{
$this->pay(Fare::fromString("50.00"));
$this->shouldBeCompletelyPaid();
}
}
class Ticket
{
private $fare;
// ...
public function pay(Fare $fare)
{
$this->fare = $this->fare->deduct($fare);
}
public function isCompletelyPaid()
{
return $this->fare->isZero();
}
}
class FareSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedFromString('100.00');
}
function it_can_deduct_an_amount()
{
$this->deduct(Fare::fromString('10'))->shouldBeLike(Fare::fromString('90.00'));
}
}
class Fare
{
private $pence;
private function __construct($pence)
{
$this->pence = $pence;
}
// ...
public function deduct(Fare $amount)
{
return new Fare($this->pence - $amount->pence);
}
}
class FareSpec extends ObjectBehavior
{
// ...
function it_knows_when_it_is_zero()
{
$this->beConstructedFromString('0.00');
$this->shouldBeZero();
}
function it_is_not_zero_when_it_has_a_value()
{
$this->beConstructedFromString('10.00');
$this->shouldNotBeZero();
}
}
class Fare
{
private $pence;
private function __construct($pence)
{
$this->pence = $pence;
}
// ...
public function isZero()
{
return $this->pence == 0;
}
}
Run Behat
class TicketIssuerSpec extends ObjectBehavior
{
function it_issues_a_ticket_with_the_correct_fare(FareList $fareList)
{
$route = Route::between(Airport::fromCode('LHR'), Airport::fromCode('MAN'));
$flight = new Flight(FlightNumber::fromString('XX001'), $route);
$fareList->findFareFor($route)->willReturn(Fare::fromString('50'));
$this->beConstructedWith($fareList);
$this->issueOn($flight)->shouldBeLike(Ticket::costing(Fare::fromString('50')));
}
}
class TicketIssuer
{
private $fareList;
public function __construct(FareList $fareList)
{
$this->fareList = $fareList;
}
public function issueOn(Flight $flight)
{
return Ticket::costing($this->fareList->findFareFor($flight->getRoute()));
}
}
interface FareList
{
public function listFare(Route $route, Fare $fare);
public function findFareFor(Route $route);
}
class FareList implements FareList
{
private $fares = [];
public function listFare(Route $route, Fare $fare)
{
$this->fares[$route->asString()] = $fare;
}
public function findFareFor(Route $route)
{
return $this->fares[$route->asString()];
}
}
Run Behat
/**
* @Then I the ticket should be worth :points loyalty points
*/
public function iTheTicketShouldBeWorthLoyaltyPoints(Points $points)
{
assert($this->ticket->getPoints() == $points);
}
class FareSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedFromString('100.00');
}
// ...
function it_calculates_points()
{
$this->getPoints()->shouldBeLike(Points::fromString('100'));
}
}
class TicketSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedCosting(Fare::fromString("100.00"));
}
// ...
function it_gets_points_from_original_fare()
{
$this->pay(Fare::fromString("50"));
$this->getPoints()->shouldBeLike(Points::fromString('100'));
}
}
<?php
class Ticket
{
private $revenueFare;
private $fare;
private function __construct(Fare $fare)
{
$this->revenueFare = $fare;
$this->fare = $fare;
}
// ...
public function getPoints()
{
return $this->revenueFare->getPoints();
}
}
Run Behat
Where is
our domain
model?
Feature: Earning and spending points on flights
Rules:
- Travellers can collect 1 point for every £1 they spend on flights
- 100 points can be redeemed for £10 off a future flight
Background:
Given a flight "XX-100" flies the "LHR" to "MAN" route
And the current listed fare for the "LHR" to "MAN" route is £50
Scenario: Earning points when paying cash
When I am issued a ticket on flight "XX-100"
And I pay £50 cash for the ticket
Then the ticket should be completely paid
And I the ticket should be worth 50 loyalty points
> bin/phpspec run -f pretty
Airport
10 ✔ can be represented as a string
16 ✔ cannot be created with invalid code
Fare
15 ✔ can deduct an amount
20 ✔ knows when it is zero
26 ✔ is not zero when it has a value
31 ✔ calculates points
FlightNumber
10 ✔ can be represented as a string
Flight
13 ✔ exposes route
Points
10 ✔ is constructed from string
Route
12 ✔ has a string representation
TicketIssuer
16 ✔ issues a ticket with the correct fare
Ticket
15 ✔ is not completely paid initially
20 ✔ is not paid completely if it is partly paid
27 ✔ can be paid completely
34 ✔ gets points from original fare
End to End
Testing
With the domain already
modelled
→ UI tests do not have to be
comprehensive
→ Can focus on intractions and UX
→ Actual UI code is easier to write!
default:
suites:
core:
contexts: [ FlightsContext ]
web:
contexts: [ WebFlightsContext ]
filters: { tags: @ui }
Feature: Earning and spending points on flights
Scenario: Earning points when paying cash
Given ...
@ui
Scenario: Redeeming points for a discount on a flight
Given ...
Scenario: Paying for a flight entirely using points
Given ...
Modelling by Example
→ Focuses attention on use cases
→ Helps developers understand core
business domains
→ Encourages layered architecture
→ Speeds up test suites
Use it when
→ Module is core to your business
→ You are likely to support business
changes in the future
→ You can have conversations with
stakeholders
Do not use when...
→ Not core to the business
→ Prototype or short-term project
→ It can be thrown away when the
business changes
→ You have no access to business experts
(but try and change this)
→ Rate talk: https://joind.in/talk/304ff
→ Join us: http://bit.ly/inviqa-careers
→ Get help: http://bit.ly/inviqa-contact

More Related Content

PDF
Modelling by Example Workshop - PHPNW 2016
CiaranMcNulty
 
PDF
Driving Design through Examples
CiaranMcNulty
 
PDF
Driving Design through Examples
CiaranMcNulty
 
PDF
Driving Design through Examples - PhpCon PL 2015
CiaranMcNulty
 
PPTX
Magento done right - PHP UK 2016
Ciaran Rooney
 
PDF
From Doctor to Coder: A Whole New World?
Aisha Sie
 
PPTX
Debugging Effectively
Colin O'Dell
 
PDF
Deploy to azure in less then 15 minutes
Michelangelo van Dam
 
Modelling by Example Workshop - PHPNW 2016
CiaranMcNulty
 
Driving Design through Examples
CiaranMcNulty
 
Driving Design through Examples
CiaranMcNulty
 
Driving Design through Examples - PhpCon PL 2015
CiaranMcNulty
 
Magento done right - PHP UK 2016
Ciaran Rooney
 
From Doctor to Coder: A Whole New World?
Aisha Sie
 
Debugging Effectively
Colin O'Dell
 
Deploy to azure in less then 15 minutes
Michelangelo van Dam
 

Viewers also liked (20)

PPTX
Programming in hack
Alejandro Marcu
 
PPTX
Hacking Your Way To Better Security - Dutch PHP Conference 2016
Colin O'Dell
 
PDF
Security Theatre - PHP UK Conference
xsist10
 
PPTX
Buscar imágenes en la web
TICS & Partners
 
PDF
Public health certificate
Monica McDaniel
 
PPTX
How Can I Convert PIO Card into OCI Card?
Services 2 NRI
 
PPT
Վեբ 2.0՝ ի՞նչ է դա
Artur Papyan
 
PDF
Conscious Decoupling - Lone Star PHP
CiaranMcNulty
 
PDF
First aid certificate 2016
Ian Simpson
 
PPTX
Social Media for the Federation of Small Businesses
This Little Piggy
 
PPTX
Administracion
Susygeo
 
PDF
Merbromin 129-16-8-api
Merbromin-129-16-8-api
 
PPTX
Crafting beautiful software
Jorn Oomen
 
PPTX
PHP Conference 2016
Edison Costa
 
PPTX
Tutorial: cómo cargar un video a Edmodo desde Youtube
TICS & Partners
 
PDF
Recommendation for Maya Emilova
MMEEVV
 
PPTX
Diseño de una guía didáctica con imágenes
TICS & Partners
 
PPTX
Scaling your website
Alejandro Marcu
 
PDF
DPC 2016 - 53 Minutes or Less - Architecting For Failure
benwaine
 
PPT
Canada Mortgage and Housing Corporation's (CMHC) Municipal Infrastructure Len...
MaRS Discovery District
 
Programming in hack
Alejandro Marcu
 
Hacking Your Way To Better Security - Dutch PHP Conference 2016
Colin O'Dell
 
Security Theatre - PHP UK Conference
xsist10
 
Buscar imágenes en la web
TICS & Partners
 
Public health certificate
Monica McDaniel
 
How Can I Convert PIO Card into OCI Card?
Services 2 NRI
 
Վեբ 2.0՝ ի՞նչ է դա
Artur Papyan
 
Conscious Decoupling - Lone Star PHP
CiaranMcNulty
 
First aid certificate 2016
Ian Simpson
 
Social Media for the Federation of Small Businesses
This Little Piggy
 
Administracion
Susygeo
 
Merbromin 129-16-8-api
Merbromin-129-16-8-api
 
Crafting beautiful software
Jorn Oomen
 
PHP Conference 2016
Edison Costa
 
Tutorial: cómo cargar un video a Edmodo desde Youtube
TICS & Partners
 
Recommendation for Maya Emilova
MMEEVV
 
Diseño de una guía didáctica con imágenes
TICS & Partners
 
Scaling your website
Alejandro Marcu
 
DPC 2016 - 53 Minutes or Less - Architecting For Failure
benwaine
 
Canada Mortgage and Housing Corporation's (CMHC) Municipal Infrastructure Len...
MaRS Discovery District
 
Ad

Similar to Driving Design through Examples (20)

PDF
4Developers 2015: Jak (w końcu) zacząć pracować z DDD wykorzystując BDD - Kac...
PROIDEA
 
PPTX
Domain-Driven Design: The "What" and the "Why"
bincangteknologi
 
PPTX
Domain Driven Design in an Agile World
Lorraine Steyn
 
PDF
DDD for real
Cyrille Martraire
 
PDF
DDD beyond the infamous repository pattern - GeeCon Prague 2018
Cyrille Martraire
 
PPTX
Behaviour driven development aka bdd
Prince Gupta
 
PPTX
Finding balance of DDD while your application grows
Carolina Karklis
 
PDF
Introduction to-ddd
John Ferguson Smart Limited
 
ODP
Into the domain
Knoldus Inc.
 
PPTX
Introduction to Domain driven design (LaravelBA #5)
guiwoda
 
PDF
Cucumber and Spock Primer
John Ferguson Smart Limited
 
PDF
Refactor your Specs - 2017 Edition
Cyrille Martraire
 
PDF
Modelling a complex domain with Domain-Driven Design
Naeem Sarfraz
 
PDF
Bounded Context - DDD Europe Foundation Track
Cyrille Martraire
 
PPT
Object Oriented Analysis and Design with UML2 part2
Haitham Raik
 
PPTX
Business driven development
Benoy John, CSM
 
PPTX
Creating a shared understanding through Story Mapping, Spec by Example, & Dom...
Jeff Anderson
 
PPTX
How to Implement Domain Driven Design in Real Life SDLC
Abdul Karim
 
PPTX
Building Maintainable PHP Applications.pptx
davorminchorov1
 
PPTX
Behavior driven development
Ritesh Mehrotra
 
4Developers 2015: Jak (w końcu) zacząć pracować z DDD wykorzystując BDD - Kac...
PROIDEA
 
Domain-Driven Design: The "What" and the "Why"
bincangteknologi
 
Domain Driven Design in an Agile World
Lorraine Steyn
 
DDD for real
Cyrille Martraire
 
DDD beyond the infamous repository pattern - GeeCon Prague 2018
Cyrille Martraire
 
Behaviour driven development aka bdd
Prince Gupta
 
Finding balance of DDD while your application grows
Carolina Karklis
 
Introduction to-ddd
John Ferguson Smart Limited
 
Into the domain
Knoldus Inc.
 
Introduction to Domain driven design (LaravelBA #5)
guiwoda
 
Cucumber and Spock Primer
John Ferguson Smart Limited
 
Refactor your Specs - 2017 Edition
Cyrille Martraire
 
Modelling a complex domain with Domain-Driven Design
Naeem Sarfraz
 
Bounded Context - DDD Europe Foundation Track
Cyrille Martraire
 
Object Oriented Analysis and Design with UML2 part2
Haitham Raik
 
Business driven development
Benoy John, CSM
 
Creating a shared understanding through Story Mapping, Spec by Example, & Dom...
Jeff Anderson
 
How to Implement Domain Driven Design in Real Life SDLC
Abdul Karim
 
Building Maintainable PHP Applications.pptx
davorminchorov1
 
Behavior driven development
Ritesh Mehrotra
 
Ad

More from CiaranMcNulty (15)

PDF
Greener web development at PHP London
CiaranMcNulty
 
PDF
Doodle Driven Development
CiaranMcNulty
 
PDF
Behat Best Practices with Symfony
CiaranMcNulty
 
PDF
Behat Best Practices
CiaranMcNulty
 
PDF
Behat Best Practices with Symfony
CiaranMcNulty
 
PDF
Conscious Coupling
CiaranMcNulty
 
PDF
Finding the Right Testing Tool for the Job
CiaranMcNulty
 
PDF
TDD with PhpSpec - Lone Star PHP 2016
CiaranMcNulty
 
PDF
Fly In Style (without splashing out)
CiaranMcNulty
 
PDF
Why Your Test Suite Sucks - PHPCon PL 2015
CiaranMcNulty
 
PDF
Building a Pyramid: Symfony Testing Strategies
CiaranMcNulty
 
PDF
TDD with PhpSpec
CiaranMcNulty
 
PDF
Why Your Test Suite Sucks
CiaranMcNulty
 
PDF
Driving Design with PhpSpec
CiaranMcNulty
 
PDF
Using HttpKernelInterface for Painless Integration
CiaranMcNulty
 
Greener web development at PHP London
CiaranMcNulty
 
Doodle Driven Development
CiaranMcNulty
 
Behat Best Practices with Symfony
CiaranMcNulty
 
Behat Best Practices
CiaranMcNulty
 
Behat Best Practices with Symfony
CiaranMcNulty
 
Conscious Coupling
CiaranMcNulty
 
Finding the Right Testing Tool for the Job
CiaranMcNulty
 
TDD with PhpSpec - Lone Star PHP 2016
CiaranMcNulty
 
Fly In Style (without splashing out)
CiaranMcNulty
 
Why Your Test Suite Sucks - PHPCon PL 2015
CiaranMcNulty
 
Building a Pyramid: Symfony Testing Strategies
CiaranMcNulty
 
TDD with PhpSpec
CiaranMcNulty
 
Why Your Test Suite Sucks
CiaranMcNulty
 
Driving Design with PhpSpec
CiaranMcNulty
 
Using HttpKernelInterface for Painless Integration
CiaranMcNulty
 

Recently uploaded (20)

PDF
Economic Impact of Data Centres to the Malaysian Economy
flintglobalapac
 
PDF
Advances in Ultra High Voltage (UHV) Transmission and Distribution Systems.pdf
Nabajyoti Banik
 
PPTX
The-Ethical-Hackers-Imperative-Safeguarding-the-Digital-Frontier.pptx
sujalchauhan1305
 
PPTX
IT Runs Better with ThousandEyes AI-driven Assurance
ThousandEyes
 
PDF
Trying to figure out MCP by actually building an app from scratch with open s...
Julien SIMON
 
PDF
Research-Fundamentals-and-Topic-Development.pdf
ayesha butalia
 
PDF
The Evolution of KM Roles (Presented at Knowledge Summit Dublin 2025)
Enterprise Knowledge
 
PDF
A Strategic Analysis of the MVNO Wave in Emerging Markets.pdf
IPLOOK Networks
 
PDF
Responsible AI and AI Ethics - By Sylvester Ebhonu
Sylvester Ebhonu
 
PPTX
What-is-the-World-Wide-Web -- Introduction
tonifi9488
 
PDF
Make GenAI investments go further with the Dell AI Factory
Principled Technologies
 
PDF
Tea4chat - another LLM Project by Kerem Atam
a0m0rajab1
 
PDF
Security features in Dell, HP, and Lenovo PC systems: A research-based compar...
Principled Technologies
 
PDF
Accelerating Oracle Database 23ai Troubleshooting with Oracle AHF Fleet Insig...
Sandesh Rao
 
PPTX
AI and Robotics for Human Well-being.pptx
JAYMIN SUTHAR
 
PDF
OFFOFFBOX™ – A New Era for African Film | Startup Presentation
ambaicciwalkerbrian
 
PDF
Using Anchore and DefectDojo to Stand Up Your DevSecOps Function
Anchore
 
PDF
The Future of Mobile Is Context-Aware—Are You Ready?
iProgrammer Solutions Private Limited
 
PDF
Cloud-Migration-Best-Practices-A-Practical-Guide-to-AWS-Azure-and-Google-Clou...
Artjoker Software Development Company
 
PDF
AI-Cloud-Business-Management-Platforms-The-Key-to-Efficiency-Growth.pdf
Artjoker Software Development Company
 
Economic Impact of Data Centres to the Malaysian Economy
flintglobalapac
 
Advances in Ultra High Voltage (UHV) Transmission and Distribution Systems.pdf
Nabajyoti Banik
 
The-Ethical-Hackers-Imperative-Safeguarding-the-Digital-Frontier.pptx
sujalchauhan1305
 
IT Runs Better with ThousandEyes AI-driven Assurance
ThousandEyes
 
Trying to figure out MCP by actually building an app from scratch with open s...
Julien SIMON
 
Research-Fundamentals-and-Topic-Development.pdf
ayesha butalia
 
The Evolution of KM Roles (Presented at Knowledge Summit Dublin 2025)
Enterprise Knowledge
 
A Strategic Analysis of the MVNO Wave in Emerging Markets.pdf
IPLOOK Networks
 
Responsible AI and AI Ethics - By Sylvester Ebhonu
Sylvester Ebhonu
 
What-is-the-World-Wide-Web -- Introduction
tonifi9488
 
Make GenAI investments go further with the Dell AI Factory
Principled Technologies
 
Tea4chat - another LLM Project by Kerem Atam
a0m0rajab1
 
Security features in Dell, HP, and Lenovo PC systems: A research-based compar...
Principled Technologies
 
Accelerating Oracle Database 23ai Troubleshooting with Oracle AHF Fleet Insig...
Sandesh Rao
 
AI and Robotics for Human Well-being.pptx
JAYMIN SUTHAR
 
OFFOFFBOX™ – A New Era for African Film | Startup Presentation
ambaicciwalkerbrian
 
Using Anchore and DefectDojo to Stand Up Your DevSecOps Function
Anchore
 
The Future of Mobile Is Context-Aware—Are You Ready?
iProgrammer Solutions Private Limited
 
Cloud-Migration-Best-Practices-A-Practical-Guide-to-AWS-Azure-and-Google-Clou...
Artjoker Software Development Company
 
AI-Cloud-Business-Management-Platforms-The-Key-to-Efficiency-Growth.pdf
Artjoker Software Development Company
 

Driving Design through Examples

  • 1. Driving Design through Examples Ciaran McNulty at Dutch PHP Conference 2016
  • 4. BDD helps with 1. Building things well 2. Building the right things 3. Building things for the right reason ... we will focus on 1 & 2
  • 5. BDD is the art of using examples in conversations to illustrate behaviour — Liz Keogh
  • 7. Requirements as Rules We are starting a new budget airline flying between London and Manchester → Travellers can collect 1 point for every £1 they spend on flights → 100 points can be redeemed for £10 off a future flight → Flights are taxed at 20%
  • 9. Ambiguity → When spending points do I still earn new points? → Can I redeem more than 100 points on one flight? → Is tax based on the discounted fare or the original price of the fare?
  • 11. Examples If a flight from London to Manchester costs £50: → If you pay cash it will cost £50 + £10 tax, and you will earn 50 new points → If you pay entirely with points it will cost 500 points + £10 tax and you will earn 0 new points → If you pay with 100 points it will cost 100 points + £40 + £10 tax and you will earn 0 new points
  • 14. You do not have to use Gherkin
  • 15. Feature: Earning and spending points on flights Rules: - Travellers can collect 1 point for every £1 they spend on flights - 100 points can be redeemed for £10 off a future flight Scenario: Earning points when paying cash Given ... Scenario: Redeeming points for a discount on a flight Given ... Scenario: Paying for a flight entirely using points Given ...
  • 16. Gherkin steps → Given sets up context for a behaviour → When specifies some action → Then specifies some outcome Action + Outcome = Behaviour
  • 17. Scenario: Earning points when paying cash Given a flight costs £50 When I pay with cash Then I should pay £50 for the flight And I should pay £10 tax And I should get 50 points Scenario: Redeeming points for a discount on a flight Given a flight costs £50 When I pay with cash plus 100 points Then I should pay £40 for the flight And I should pay £10 tax And I should pay 100 points Scenario: Paying for a flight entirely using points Given a flight costs £50 When I pay with points only Then I should pay £0 for the flight And I should pay £10 tax And I should pay 500 points
  • 18. Who writes examples? Business expert Testing expert Development expert All discussing the feature together
  • 19. When to write scenarios → Before you start work on the feature → Not too long before! → Whenever you have access to the right people
  • 20. Refining scenarios → When would this outcome not be true? → What other outcomes are there? → But what would happen if...? → Does this implementation detail matter?
  • 22. Scenarios → Create a shared understanding of a feature → Give a starting definition of done → Provide an objective indication of how to test a feature
  • 24. DDD tackles complexity by focusing the team's attention on knowledge of the domain — Eric Evans
  • 26. Ubiquitous Language → A shared way of speaking about domain concepts → Reduces the cost of translation when business and development communicate → Try to establish and use terms the business will understand
  • 28. By embedding Ubiquitous Language in your scenarios, your scenarios naturally become your domain model — Konstantin Kudryashov (@everzet)
  • 29. Principles → The best way to understand the domain is by discussing examples → Write scenarios that capture ubiquitous language → Write scenarios that illustrate real situations → Directly drive the code model from those examples
  • 33. Testing through the UI → Slow to execute → Brittle → Makes you design the domain and UI at the same time
  • 34. Test the domain first
  • 35. Testing with real infrastructure → Slow to execute → Brittle → Makes you design the domain and infrastructure at the same time
  • 36. Test with fake infrastructure first
  • 38. Scenario: Earning points when paying cash Given a flight costs £50 When I pay with cash Then I should pay £50 for the flight And I should pay £10 tax And I should get 50 points Scenario: Redeeming points for a discount on a flight Given a flight costs £50 When I pay with cash plus 100 points Then I should pay £40 for the flight And I should pay £10 tax And I should pay 100 points Scenario: Paying for a flight entirely using points Given a flight costs £50 When I pay with points only Then I should pay £0 for the flight And I should pay £10 tax And I should pay 500 points
  • 40. Background: Given a flight from "London" to "Manchester" costs £50 Scenario: Earning points when paying cash When I fly from "London" to "Manchester" And I pay with cash Then I should pay £50 for the flight And I should pay £10 tax And I should get 50 points
  • 42. → What words do you use to talk about these things? → Points? Paying? Cash Fly? → Is the cost really attached to a flight? → Do you call this thing "tax"? → How do you think about these things?
  • 44. Lessons from the conversation → Price belongs to a Fare for a specific Route → Flight is independently assigned to a Route → Some sort of fare listing system controls Fares → I get quoted a cost at the point I purchase a ticket This is really useful to know!
  • 45. Background: Given a flight "XX-100" flies the "LHR" to "MAN" route And the current listed fare for the "LHR" to "MAN" route is £50 Scenario: Earning points when paying cash When I am issued a ticket on flight "XX-100" And I pay £50 cash for the ticket Then the ticket should be completely paid And the ticket should be worth 50 loyalty points
  • 47. Configure a Behat suite default: suites: core: contexts: [ FlightsContext ]
  • 48. Create a context class FlightsContext implements Context { /** * @Given a flight :arg1 flies the :arg2 to :arg3 route */ public function aFlightFliesTheRoute($arg1, $arg2, $arg3) { throw new PendingException(); } // ... }
  • 51. class FlightsContext implements Context { /** * @Given a flight :flightnumber flies the :origin to :destination route */ public function aFlightFliesTheRoute($flightnumber, $origin, $destination) { $this->flight = new Flight( FlightNumber::fromString($flightnumber), Route::between( Airport::fromCode($origin), Airport::fromCode($destination) ) ); } // ... }
  • 52. Transformations /** * @Transform :flightnumber */ public function transformFlightNumber($number) { return FlightNumber::fromString($number); } /** * @Transform :origin * @Transform :destination */ public function transformAirport($code) { return Airport::fromCode($code); }
  • 53. /** * @Given a flight :flightnumber flies the :origin to :destination route */ public function aFlightFliesTheRoute( FlightNumber $flightnumber, Airport $origin, Airport $destination ) { $this->flight = new Flight( $flightnumber, Route::between($origin, $destination) ); }
  • 54. > vendor/bin/behat PHP Fatal error: Class 'Flight' not found
  • 56. class AirportSpec extends ObjectBehavior { function it_can_be_represented_as_a_string() { $this->beConstructedFromCode('LHR'); $this->asCode()->shouldReturn('LHR'); } function it_cannot_be_created_with_invalid_code() { $this->beConstructedFromCode('1234566XXX'); $this->shouldThrow(Exception::class)->duringInstantiation(); } }
  • 57. class Airport { private $code; private function __construct($code) { if (!preg_match('/^[A-Z]{3}$/', $code)) { throw new InvalidArgumentException('Code is not valid'); } $this->code = $code; } public static function fromCode($code) { return new Airport($code); } public function asCode() { return $this->code; } }
  • 59. /** * @Given the current listed fare for the :arg1 to :arg2 route is £:arg3 */ public function theCurrentListedFareForTheToRouteIsPs($arg1, $arg2, $arg3) { throw new PendingException(); }
  • 61. interface FareList { public function listFare(Route $route, Fare $fare); }
  • 62. Create in-memory versions for testing namespace Fake; class FareList implements FareList { private $fares = []; public function listFare(Route $route, Fare $fare) { $this->fares[$route->asString()] = $fare; } }
  • 63. /** * @Given the current listed fare for the :origin to :destination route is £:fare */ public function theCurrentListedFareForTheToRouteIsPs( Airport $origin, Airport $destination, Fare $fare ) { $this->fareList = new FakeFareList(); $this->fareList->listFare( Route::between($origin, $destination), Fare::fromString($fare) ); }
  • 65. /** * @When Iam issued a ticket on flight :arg1 */ public function iAmIssuedATicketOnFlight($arg1) { throw new PendingException(); }
  • 66. /** * @When I am issued a ticket on flight :flight */ public function iAmIssuedATicketOnFlight() { $ticketIssuer = new TicketIssuer($this->fareList); $this->ticket = $ticketIssuer->issueOn($this->flight); }
  • 67. > vendor/bin/behat PHP Fatal error: Class 'TicketIssuer' not found
  • 68. class TicketIssuerSpec extends ObjectBehavior { function it_can_issue_a_ticket_for_a_flight(Flight $flight) { $this->issueOn($flight)->shouldHaveType(Ticket::class); } }
  • 69. class TicketIssuer { public function issueOn(Flight $flight) { return Ticket::costing(Fare::fromString('10000.00')); } }
  • 71. /** * @When I pay £:fare cash for the ticket */ public function iPayPsCashForTheTicket(Fare $fare) { $this->ticket->pay($fare); }
  • 72. PHP Fatal error: Call to undefined method Ticket::pay()
  • 73. class TicketSpec extends ObjectBehavior { function it_can_be_paid() { $this->pay(Fare::fromString("10.00")); } }
  • 74. class Ticket { public function pay(Fare $fare) { } }
  • 76. The model will be anaemicUntil you get to Then
  • 77. /** * @Then the ticket should be completely paid */ public function theTicketShouldBeCompletelyPaid() { throw new PendingException(); }
  • 78. /** * @Then the ticket should be completely paid */ public function theTicketShouldBeCompletelyPaid() { assert($this->ticket->isCompletelyPaid() == true); }
  • 79. PHP Fatal error: Call to undefined method Ticket::isCompletelyPaid()
  • 80. class TicketSpec extends ObjectBehavior { function let() { $this->beConstructedCosting(Fare::fromString("50.00")); } function it_is_not_completely_paid_initially() { $this->shouldNotBeCompletelyPaid(); } function it_can_be_paid_completely() { $this->pay(Fare::fromString("50.00")); $this->shouldBeCompletelyPaid(); } }
  • 81. class Ticket { private $fare; // ... public function pay(Fare $fare) { $this->fare = $this->fare->deduct($fare); } public function isCompletelyPaid() { return $this->fare->isZero(); } }
  • 82. class FareSpec extends ObjectBehavior { function let() { $this->beConstructedFromString('100.00'); } function it_can_deduct_an_amount() { $this->deduct(Fare::fromString('10'))->shouldBeLike(Fare::fromString('90.00')); } }
  • 83. class Fare { private $pence; private function __construct($pence) { $this->pence = $pence; } // ... public function deduct(Fare $amount) { return new Fare($this->pence - $amount->pence); } }
  • 84. class FareSpec extends ObjectBehavior { // ... function it_knows_when_it_is_zero() { $this->beConstructedFromString('0.00'); $this->shouldBeZero(); } function it_is_not_zero_when_it_has_a_value() { $this->beConstructedFromString('10.00'); $this->shouldNotBeZero(); } }
  • 85. class Fare { private $pence; private function __construct($pence) { $this->pence = $pence; } // ... public function isZero() { return $this->pence == 0; } }
  • 87. class TicketIssuerSpec extends ObjectBehavior { function it_issues_a_ticket_with_the_correct_fare(FareList $fareList) { $route = Route::between(Airport::fromCode('LHR'), Airport::fromCode('MAN')); $flight = new Flight(FlightNumber::fromString('XX001'), $route); $fareList->findFareFor($route)->willReturn(Fare::fromString('50')); $this->beConstructedWith($fareList); $this->issueOn($flight)->shouldBeLike(Ticket::costing(Fare::fromString('50'))); } }
  • 88. class TicketIssuer { private $fareList; public function __construct(FareList $fareList) { $this->fareList = $fareList; } public function issueOn(Flight $flight) { return Ticket::costing($this->fareList->findFareFor($flight->getRoute())); } }
  • 89. interface FareList { public function listFare(Route $route, Fare $fare); public function findFareFor(Route $route); }
  • 90. class FareList implements FareList { private $fares = []; public function listFare(Route $route, Fare $fare) { $this->fares[$route->asString()] = $fare; } public function findFareFor(Route $route) { return $this->fares[$route->asString()]; } }
  • 92. /** * @Then I the ticket should be worth :points loyalty points */ public function iTheTicketShouldBeWorthLoyaltyPoints(Points $points) { assert($this->ticket->getPoints() == $points); }
  • 93. class FareSpec extends ObjectBehavior { function let() { $this->beConstructedFromString('100.00'); } // ... function it_calculates_points() { $this->getPoints()->shouldBeLike(Points::fromString('100')); } }
  • 94. class TicketSpec extends ObjectBehavior { function let() { $this->beConstructedCosting(Fare::fromString("100.00")); } // ... function it_gets_points_from_original_fare() { $this->pay(Fare::fromString("50")); $this->getPoints()->shouldBeLike(Points::fromString('100')); } }
  • 95. <?php class Ticket { private $revenueFare; private $fare; private function __construct(Fare $fare) { $this->revenueFare = $fare; $this->fare = $fare; } // ... public function getPoints() { return $this->revenueFare->getPoints(); } }
  • 98. Feature: Earning and spending points on flights Rules: - Travellers can collect 1 point for every £1 they spend on flights - 100 points can be redeemed for £10 off a future flight Background: Given a flight "XX-100" flies the "LHR" to "MAN" route And the current listed fare for the "LHR" to "MAN" route is £50 Scenario: Earning points when paying cash When I am issued a ticket on flight "XX-100" And I pay £50 cash for the ticket Then the ticket should be completely paid And I the ticket should be worth 50 loyalty points
  • 99. > bin/phpspec run -f pretty Airport 10 ✔ can be represented as a string 16 ✔ cannot be created with invalid code Fare 15 ✔ can deduct an amount 20 ✔ knows when it is zero 26 ✔ is not zero when it has a value 31 ✔ calculates points FlightNumber 10 ✔ can be represented as a string Flight 13 ✔ exposes route Points 10 ✔ is constructed from string Route 12 ✔ has a string representation TicketIssuer 16 ✔ issues a ticket with the correct fare Ticket 15 ✔ is not completely paid initially 20 ✔ is not paid completely if it is partly paid 27 ✔ can be paid completely 34 ✔ gets points from original fare
  • 101. With the domain already modelled → UI tests do not have to be comprehensive → Can focus on intractions and UX → Actual UI code is easier to write!
  • 102. default: suites: core: contexts: [ FlightsContext ] web: contexts: [ WebFlightsContext ] filters: { tags: @ui }
  • 103. Feature: Earning and spending points on flights Scenario: Earning points when paying cash Given ... @ui Scenario: Redeeming points for a discount on a flight Given ... Scenario: Paying for a flight entirely using points Given ...
  • 104. Modelling by Example → Focuses attention on use cases → Helps developers understand core business domains → Encourages layered architecture → Speeds up test suites
  • 105. Use it when → Module is core to your business → You are likely to support business changes in the future → You can have conversations with stakeholders
  • 106. Do not use when... → Not core to the business → Prototype or short-term project → It can be thrown away when the business changes → You have no access to business experts (but try and change this)
  • 107. → Rate talk: https://joind.in/talk/304ff → Join us: http://bit.ly/inviqa-careers → Get help: http://bit.ly/inviqa-contact