SlideShare uma empresa Scribd logo
1 de 108
Baixar para ler offline
Driving Design through Examples
Ciaran McNulty at PHP UK 2017
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 ïŹ‚ying between
London and Manchester
→ Travellers can collect 1 point for every £1 they
spend on ïŹ‚ights
→ 100 points can be redeemed for £10 off a future
ïŹ‚ight
→ 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 ïŹ‚ight?
→ Is tax based on the discounted fare or the
original price of the fare?
Examples are
Unambiguous
Examples
If a ïŹ‚ight 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 speciïŹes some action
→ Then speciïŹes 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
ReïŹning 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 deïŹnition 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 ïŹrst
Testing with real infrastructure
→ Slow to execute
→ Brittle
→ Makes you design the domain and infrastructure
at the same time
Test with fake infrastructure ïŹrst
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 ïŹ‚ight?
→ 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 speciïŹc 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
ConïŹgure 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;
}
}
/**
* @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)
Thank you
https://joind.in/talk/c52f8
Thank you
https://joind.in/talk/c52f8

Mais conteĂșdo relacionado

Destaque

Destaque (20)

Hopping in clouds - phpuk 17
Hopping in clouds - phpuk 17Hopping in clouds - phpuk 17
Hopping in clouds - phpuk 17
 
Conscious Coupling
Conscious CouplingConscious Coupling
Conscious Coupling
 
PHP UK 2017 - Don't Lose Sleep - Secure Your REST
PHP UK 2017 - Don't Lose Sleep - Secure Your RESTPHP UK 2017 - Don't Lose Sleep - Secure Your REST
PHP UK 2017 - Don't Lose Sleep - Secure Your REST
 
Preparing your dockerised application for production deployment
Preparing your dockerised application for production deploymentPreparing your dockerised application for production deployment
Preparing your dockerised application for production deployment
 
Drupal8 for Symfony Developers
Drupal8 for Symfony DevelopersDrupal8 for Symfony Developers
Drupal8 for Symfony Developers
 
Integrating React.js with PHP projects
Integrating React.js with PHP projectsIntegrating React.js with PHP projects
Integrating React.js with PHP projects
 
WordPress for the modern PHP developer
WordPress for the modern PHP developerWordPress for the modern PHP developer
WordPress for the modern PHP developer
 
Intro to Debugging PHP with Xdebug
Intro to Debugging PHP with XdebugIntro to Debugging PHP with Xdebug
Intro to Debugging PHP with Xdebug
 
Essential debugging php debugging techniques, tips & tricks
Essential debugging php debugging techniques, tips & tricksEssential debugging php debugging techniques, tips & tricks
Essential debugging php debugging techniques, tips & tricks
 
Debugging PHP With Xdebug
Debugging PHP With XdebugDebugging PHP With Xdebug
Debugging PHP With Xdebug
 
How the real-time communication between things can simplify our everyday lif...
How the real-time communication between things can simplify  our everyday lif...How the real-time communication between things can simplify  our everyday lif...
How the real-time communication between things can simplify our everyday lif...
 
Advanced debugging techniques (PHP)
Advanced debugging techniques (PHP)Advanced debugging techniques (PHP)
Advanced debugging techniques (PHP)
 
Demystifying Object-Oriented Programming - PHP UK Conference 2017
Demystifying Object-Oriented Programming - PHP UK Conference 2017Demystifying Object-Oriented Programming - PHP UK Conference 2017
Demystifying Object-Oriented Programming - PHP UK Conference 2017
 
PHPBLT#6 PHPたæœȘæ„ă«ć…„ă‚‹ă‹ă‚‚ă—ă‚ŒăȘă„æ©ŸèƒœăźçŽč介
PHPBLT#6 PHPたæœȘæ„ă«ć…„ă‚‹ă‹ă‚‚ă—ă‚ŒăȘă„æ©ŸèƒœăźçŽč介PHPBLT#6 PHPたæœȘæ„ă«ć…„ă‚‹ă‹ă‚‚ă—ă‚ŒăȘă„æ©ŸèƒœăźçŽč介
PHPBLT#6 PHPたæœȘæ„ă«ć…„ă‚‹ă‹ă‚‚ă—ă‚ŒăȘă„æ©ŸèƒœăźçŽč介
 
PHP data structures (and the impact of php 7 on them), phpDay Verona 2015, Italy
PHP data structures (and the impact of php 7 on them), phpDay Verona 2015, ItalyPHP data structures (and the impact of php 7 on them), phpDay Verona 2015, Italy
PHP data structures (and the impact of php 7 on them), phpDay Verona 2015, Italy
 
Kicking off with Zend Expressive and Doctrine ORM (PHP UK 2017)
Kicking off with Zend Expressive and Doctrine ORM (PHP UK 2017)Kicking off with Zend Expressive and Doctrine ORM (PHP UK 2017)
Kicking off with Zend Expressive and Doctrine ORM (PHP UK 2017)
 
Debugging
DebuggingDebugging
Debugging
 
Zend Framework Foundations
Zend Framework FoundationsZend Framework Foundations
Zend Framework Foundations
 
Create, test, secure, repeat
Create, test, secure, repeatCreate, test, secure, repeat
Create, test, secure, repeat
 
Amp your site an intro to accelerated mobile pages
Amp your site  an intro to accelerated mobile pagesAmp your site  an intro to accelerated mobile pages
Amp your site an intro to accelerated mobile pages
 

Semelhante a Driving Design through Examples

Assignment 1 Expository Essay IntroductionIn this assignment, .docx
Assignment 1 Expository Essay IntroductionIn this assignment, .docxAssignment 1 Expository Essay IntroductionIn this assignment, .docx
Assignment 1 Expository Essay IntroductionIn this assignment, .docx
sherni1
 
Cost of Capital and its different types of cost of capital
Cost of Capital and  its different types of cost of capitalCost of Capital and  its different types of cost of capital
Cost of Capital and its different types of cost of capital
VadivelM9
 

Semelhante a Driving Design through Examples (11)

Cucumber and Spock Primer
Cucumber and Spock PrimerCucumber and Spock Primer
Cucumber and Spock Primer
 
Assignment 1 Expository Essay IntroductionIn this assignment, .docx
Assignment 1 Expository Essay IntroductionIn this assignment, .docxAssignment 1 Expository Essay IntroductionIn this assignment, .docx
Assignment 1 Expository Essay IntroductionIn this assignment, .docx
 
Refactor your Specs - 2017 Edition
Refactor your Specs - 2017 EditionRefactor your Specs - 2017 Edition
Refactor your Specs - 2017 Edition
 
Rethinking flight shopping with conversational search with chatbots
Rethinking flight shopping with conversational search with chatbotsRethinking flight shopping with conversational search with chatbots
Rethinking flight shopping with conversational search with chatbots
 
Rethinking Flight Shopping With Conversational Search... With Chatbots
Rethinking Flight Shopping With Conversational Search... With ChatbotsRethinking Flight Shopping With Conversational Search... With Chatbots
Rethinking Flight Shopping With Conversational Search... With Chatbots
 
Pricing Analytics: Segmenting Customers To Maximize Revenue
Pricing Analytics: Segmenting Customers To Maximize RevenuePricing Analytics: Segmenting Customers To Maximize Revenue
Pricing Analytics: Segmenting Customers To Maximize Revenue
 
4Developers 2015: Jak (w koƄcu) zacząć pracować z DDD wykorzystując BDD - Kac...
4Developers 2015: Jak (w koƄcu) zacząć pracować z DDD wykorzystując BDD - Kac...4Developers 2015: Jak (w koƄcu) zacząć pracować z DDD wykorzystując BDD - Kac...
4Developers 2015: Jak (w koƄcu) zacząć pracować z DDD wykorzystując BDD - Kac...
 
Engage! Bringing teams together to deliver software that makes a difference
Engage! Bringing teams together to deliver software that makes a differenceEngage! Bringing teams together to deliver software that makes a difference
Engage! Bringing teams together to deliver software that makes a difference
 
Constance et qualité du code dans une équipe - Rémi Prévost
Constance et qualité du code dans une équipe - Rémi PrévostConstance et qualité du code dans une équipe - Rémi Prévost
Constance et qualité du code dans une équipe - Rémi Prévost
 
Multi Stop Flights.docx
Multi Stop Flights.docxMulti Stop Flights.docx
Multi Stop Flights.docx
 
Cost of Capital and its different types of cost of capital
Cost of Capital and  its different types of cost of capitalCost of Capital and  its different types of cost of capital
Cost of Capital and its different types of cost of capital
 

Mais de CiaranMcNulty

Mais de CiaranMcNulty (15)

Greener web development at PHP London
Greener web development at PHP LondonGreener web development at PHP London
Greener web development at PHP London
 
Doodle Driven Development
Doodle Driven DevelopmentDoodle Driven Development
Doodle Driven Development
 
Behat Best Practices with Symfony
Behat Best Practices with SymfonyBehat Best Practices with Symfony
Behat Best Practices with Symfony
 
Behat Best Practices
Behat Best PracticesBehat Best Practices
Behat Best Practices
 
Behat Best Practices with Symfony
Behat Best Practices with SymfonyBehat Best Practices with Symfony
Behat Best Practices with Symfony
 
Finding the Right Testing Tool for the Job
Finding the Right Testing Tool for the JobFinding the Right Testing Tool for the Job
Finding the Right Testing Tool for the Job
 
Conscious Decoupling - Lone Star PHP
Conscious Decoupling - Lone Star PHPConscious Decoupling - Lone Star PHP
Conscious Decoupling - Lone Star PHP
 
TDD with PhpSpec - Lone Star PHP 2016
TDD with PhpSpec - Lone Star PHP 2016TDD with PhpSpec - Lone Star PHP 2016
TDD with PhpSpec - Lone Star PHP 2016
 
Fly In Style (without splashing out)
Fly In Style (without splashing out)Fly In Style (without splashing out)
Fly In Style (without splashing out)
 
Why Your Test Suite Sucks - PHPCon PL 2015
Why Your Test Suite Sucks - PHPCon PL 2015Why Your Test Suite Sucks - PHPCon PL 2015
Why Your Test Suite Sucks - PHPCon PL 2015
 
Building a Pyramid: Symfony Testing Strategies
Building a Pyramid: Symfony Testing StrategiesBuilding a Pyramid: Symfony Testing Strategies
Building a Pyramid: Symfony Testing Strategies
 
TDD with PhpSpec
TDD with PhpSpecTDD with PhpSpec
TDD with PhpSpec
 
Why Your Test Suite Sucks
Why Your Test Suite SucksWhy Your Test Suite Sucks
Why Your Test Suite Sucks
 
Driving Design with PhpSpec
Driving Design with PhpSpecDriving Design with PhpSpec
Driving Design with PhpSpec
 
Using HttpKernelInterface for Painless Integration
Using HttpKernelInterface for Painless IntegrationUsing HttpKernelInterface for Painless Integration
Using HttpKernelInterface for Painless Integration
 

Último

Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Safe Software
 
+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...
+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...
+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...
?#DUbAI#??##{{(☎+971_581248768%)**%*]'#abortion pills for sale in dubai@
 
Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
WSO2
 

Último (20)

FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024FWD Group - Insurer Innovation Award 2024
FWD Group - Insurer Innovation Award 2024
 
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
Connector Corner: Accelerate revenue generation using UiPath API-centric busi...
 
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
Apidays New York 2024 - The Good, the Bad and the Governed by David O'Neill, ...
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected Worker
 
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers:  A Deep Dive into Serverless Spatial Data and FMECloud Frontiers:  A Deep Dive into Serverless Spatial Data and FME
Cloud Frontiers: A Deep Dive into Serverless Spatial Data and FME
 
Navi Mumbai Call Girls đŸ„° 8617370543 Service Offer VIP Hot Model
Navi Mumbai Call Girls đŸ„° 8617370543 Service Offer VIP Hot ModelNavi Mumbai Call Girls đŸ„° 8617370543 Service Offer VIP Hot Model
Navi Mumbai Call Girls đŸ„° 8617370543 Service Offer VIP Hot Model
 
Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024Axa Assurance Maroc - Insurer Innovation Award 2024
Axa Assurance Maroc - Insurer Innovation Award 2024
 
+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...
+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...
+971581248768>> SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHA...
 
presentation ICT roal in 21st century education
presentation ICT roal in 21st century educationpresentation ICT roal in 21st century education
presentation ICT roal in 21st century education
 
Real Time Object Detection Using Open CV
Real Time Object Detection Using Open CVReal Time Object Detection Using Open CV
Real Time Object Detection Using Open CV
 
DBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor PresentationDBX First Quarter 2024 Investor Presentation
DBX First Quarter 2024 Investor Presentation
 
AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024AXA XL - Insurer Innovation Award Americas 2024
AXA XL - Insurer Innovation Award Americas 2024
 
Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...Apidays New York 2024 - The value of a flexible API Management solution for O...
Apidays New York 2024 - The value of a flexible API Management solution for O...
 
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemkeProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
ProductAnonymous-April2024-WinProductDiscovery-MelissaKlemke
 
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
Apidays New York 2024 - Accelerating FinTech Innovation by Vasa Krishnan, Fin...
 
A Year of the Servo Reboot: Where Are We Now?
A Year of the Servo Reboot: Where Are We Now?A Year of the Servo Reboot: Where Are We Now?
A Year of the Servo Reboot: Where Are We Now?
 
Corporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptxCorporate and higher education May webinar.pptx
Corporate and higher education May webinar.pptx
 
"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
"I see eyes in my soup": How Delivery Hero implemented the safety system for ..."I see eyes in my soup": How Delivery Hero implemented the safety system for ...
"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
 
Artificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : UncertaintyArtificial Intelligence Chap.5 : Uncertainty
Artificial Intelligence Chap.5 : Uncertainty
 
Architecting Cloud Native Applications
Architecting Cloud Native ApplicationsArchitecting Cloud Native Applications
Architecting Cloud Native Applications
 

Driving Design through Examples

  • 1. Driving Design through Examples Ciaran McNulty at PHP UK 2017
  • 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 ïŹ‚ying between London and Manchester → Travellers can collect 1 point for every ÂŁ1 they spend on ïŹ‚ights → 100 points can be redeemed for ÂŁ10 off a future ïŹ‚ight → 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 ïŹ‚ight? → Is tax based on the discounted fare or the original price of the fare?
  • 11. Examples If a ïŹ‚ight 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 speciïŹes some action → Then speciïŹes 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. ReïŹning 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 deïŹnition 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 ïŹrst
  • 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 ïŹrst
  • 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 ïŹ‚ight? → 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 speciïŹc 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. ConïŹgure 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; } }
  • 58.
  • 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)