driving design through examples - phpcon pl 2015

104
Driving Design through Examples Ciaran McNulty at PHPCon Poland 2015

Upload: ciaranmcnulty

Post on 18-Jan-2017

546 views

Category:

Software


2 download

TRANSCRIPT

Page 1: Driving Design through Examples - PhpCon PL 2015

Driving Design through ExamplesCiaran McNulty at PHPCon Poland 2015

Page 2: Driving Design through Examples - PhpCon PL 2015

Modelling by Example

Combining BDD and DDD concepts

Page 3: Driving Design through Examples - PhpCon PL 2015

Behaviour Driven

Development

Page 4: Driving Design through Examples - PhpCon PL 2015

BDD helps with1. Building things well

2. Building the right things3. Building things for the right reason

... we will focus on 1 & 2

Page 5: Driving Design through Examples - PhpCon PL 2015

BDD is the art of using examples in

conversations to illustrate behaviour

— Liz Keogh

Page 6: Driving Design through Examples - PhpCon PL 2015

Why Examples?

Page 7: Driving Design through Examples - PhpCon PL 2015

Requirements as RulesWe are starting a new budget airline flying between

London and Warsaw→ 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%

Page 8: Driving Design through Examples - PhpCon PL 2015

Rules are Ambiguous

Page 9: Driving Design through Examples - PhpCon PL 2015

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?

Page 10: Driving Design through Examples - PhpCon PL 2015

Examples are Unambiguous

Page 11: Driving Design through Examples - PhpCon PL 2015

ExamplesIf a flight from London to Warsaw 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

Page 12: Driving Design through Examples - PhpCon PL 2015

Examples are Objectively

Testable

Page 13: Driving Design through Examples - PhpCon PL 2015

GherkinA formal language for

examples

Page 14: Driving Design through Examples - PhpCon PL 2015

You do not have to use

Gherkin

Page 15: Driving Design through Examples - PhpCon PL 2015

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 ...

Page 16: Driving Design through Examples - PhpCon PL 2015

Gherkin steps→ Given sets up context for a behaviour

→ When specifies some action→ Then specifies some outcome

Action + Outcome = Behaviour

Page 17: Driving Design through Examples - PhpCon PL 2015

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

Page 18: Driving Design through Examples - PhpCon PL 2015

Who writes examples?Business expertTesting expert

Development expertAll discussing the feature together

Page 19: Driving Design through Examples - PhpCon PL 2015

When to write scenarios→ Before you start work on the feature

→ Not too long before!→ Whenever you have access to the right people

Page 20: Driving Design through Examples - PhpCon PL 2015

Refining scenarios→ When would this outcome not be true?

→ What other outcomes are there?→ But what would happen if...?

→ Does this implementation detail matter?

Page 21: Driving Design through Examples - PhpCon PL 2015

Scenarios are not Contracts

Page 22: Driving Design through Examples - PhpCon PL 2015

Scenarios→ Create a shared understanding of a feature

→ Give a starting definition of done→ Provide an objective indication of how to test a

feature

Page 23: Driving Design through Examples - PhpCon PL 2015

Domain Driven Design

Page 24: Driving Design through Examples - PhpCon PL 2015

DDD tackles complexity by focusing the team's

attention on knowledge of the domain

— Eric Evans

Page 25: Driving Design through Examples - PhpCon PL 2015

Invest time inunderstandingthe business

Page 26: Driving Design through Examples - PhpCon PL 2015

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

Page 27: Driving Design through Examples - PhpCon PL 2015

Modelling by Example

Page 28: Driving Design through Examples - PhpCon PL 2015

By embedding Ubiquitous Language in your scenarios,

your scenarios naturally become your domain model

— Konstantin Kudryashov (@everzet)

Page 29: Driving Design through Examples - PhpCon PL 2015

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

Page 30: Driving Design through Examples - PhpCon PL 2015

Directly driving code with Behat?

Page 31: Driving Design through Examples - PhpCon PL 2015

Layered architecture

Page 32: Driving Design through Examples - PhpCon PL 2015

UI testing with Behat

Page 33: Driving Design through Examples - PhpCon PL 2015

UI tests→ Slow to execute

→ Brittle→ Don't let you think about the code

Page 34: Driving Design through Examples - PhpCon PL 2015

Test the domain first

Page 35: Driving Design through Examples - PhpCon PL 2015

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

Page 36: Driving Design through Examples - PhpCon PL 2015

Add realistic details

Page 37: Driving Design through Examples - PhpCon PL 2015

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

Page 38: Driving Design through Examples - PhpCon PL 2015

Actively seek terms from the domain

Page 39: Driving Design through Examples - PhpCon PL 2015

→ 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?

Page 40: Driving Design through Examples - PhpCon PL 2015

Get good at listening

Page 41: Driving Design through Examples - PhpCon PL 2015

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 ticketThis is really useful to know!

Page 42: Driving Design through Examples - PhpCon PL 2015

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

Page 43: Driving Design through Examples - PhpCon PL 2015

Driving the domain model

with Behat

Page 44: Driving Design through Examples - PhpCon PL 2015

Configure a Behat suitedefault: suites: core: contexts: [ FlightsContext ]

Page 45: Driving Design through Examples - PhpCon PL 2015

Create a contextclass FlightsContext implements Context{ /** * @Given a flight :arg1 flies the :arg2 to :arg3 route */ public function aFlightFliesTheRoute($arg1, $arg2, $arg3) { throw new PendingException(); }

// ...}

Page 46: Driving Design through Examples - PhpCon PL 2015

Run Behat

Page 47: Driving Design through Examples - PhpCon PL 2015

Model values as Value Objects

Page 48: Driving Design through Examples - PhpCon PL 2015

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) ) ); }

// ...}

Page 49: Driving Design through Examples - PhpCon PL 2015

Transformations/** * @Transform :flightnumber */public function transformFlightNumber($number){ return FlightNumber::fromString($number);}

/** * @Transform :origin * @Transform :destination */public function transformAirport($code){ return Airport::fromCode($code);}

Page 50: Driving Design through Examples - PhpCon PL 2015

/** * @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) );}

Page 51: Driving Design through Examples - PhpCon PL 2015

> vendor/bin/behatPHP Fatal error: Class 'Flight' not found

Page 52: Driving Design through Examples - PhpCon PL 2015

Describe objects with

PhpSpec

Page 53: Driving Design through Examples - PhpCon PL 2015

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(); }}

Page 54: Driving Design through Examples - PhpCon PL 2015

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; }}

Page 55: Driving Design through Examples - PhpCon PL 2015
Page 56: Driving Design through Examples - PhpCon PL 2015

/** * @Given the current listed fare for the :arg1 to :arg2 route is £:arg3 */public function theCurrentListedFareForTheToRouteIsPs($arg1, $arg2, $arg3){ throw new PendingException();}

Page 57: Driving Design through Examples - PhpCon PL 2015

Model boundarieswith Interfaces

Page 58: Driving Design through Examples - PhpCon PL 2015

interface FareList{ public function listFare(Route $route, Fare $fare);}

Page 59: Driving Design through Examples - PhpCon PL 2015

Create in-memory versions for testingnamespace Fake;

class FareList implements \FareList{ private $fares = [];

public function listFare(\Route $route, \Fare $fare) { $this->fares[$route->asString()] = $fare; }}

Page 60: Driving Design through Examples - PhpCon PL 2015

/** * @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 Fake\FareList(); $this->fareList->listFare( Route::between($origin, $destination), Fare::fromString($fare) );}

Page 61: Driving Design through Examples - PhpCon PL 2015

Run Behat

Page 62: Driving Design through Examples - PhpCon PL 2015

/** * @When Iam issued a ticket on flight :arg1 */public function iAmIssuedATicketOnFlight($arg1){ throw new PendingException();}

Page 63: Driving Design through Examples - PhpCon PL 2015

/** * @When I am issued a ticket on flight :flight */public function iAmIssuedATicketOnFlight(){ $ticketIssuer = new TicketIssuer($this->fareList); $this->ticket = $ticketIssuer->issueOn($this->flight);}

Page 64: Driving Design through Examples - PhpCon PL 2015

> vendor/bin/behatPHP Fatal error: Class 'TicketIssuer' not found

Page 65: Driving Design through Examples - PhpCon PL 2015

class TicketIssuerSpec extends ObjectBehavior{ function it_can_issue_a_ticket_for_a_flight(\Flight $flight) { $this->issueOn($flight)->shouldHaveType(\Ticket::class); }}

Page 66: Driving Design through Examples - PhpCon PL 2015

class TicketIssuer{ public function issueOn(Flight $flight) { return Ticket::costing(Fare::fromString('10000.00')); }}

Page 67: Driving Design through Examples - PhpCon PL 2015

Run Behat

Page 68: Driving Design through Examples - PhpCon PL 2015

/** * @When I pay £:fare cash for the ticket */public function iPayPsCashForTheTicket(Fare $fare){ $this->ticket->pay($fare);}

Page 69: Driving Design through Examples - PhpCon PL 2015

PHP Fatal error: Call to undefined method Ticket::pay()

Page 70: Driving Design through Examples - PhpCon PL 2015

class TicketSpec extends ObjectBehavior{ function it_can_be_paid() { $this->pay(\Fare::fromString("10.00")); }}

Page 71: Driving Design through Examples - PhpCon PL 2015

class Ticket{ public function pay(Fare $fare) { }}

Page 72: Driving Design through Examples - PhpCon PL 2015

Run Behat

Page 73: Driving Design through Examples - PhpCon PL 2015

The model will beanaemicUntil you get to Then

Page 74: Driving Design through Examples - PhpCon PL 2015

/** * @Then the ticket should be completely paid */public function theTicketShouldBeCompletelyPaid(){ throw new PendingException();}

Page 75: Driving Design through Examples - PhpCon PL 2015

/** * @Then the ticket should be completely paid */public function theTicketShouldBeCompletelyPaid(){ assert($this->ticket->isCompletelyPaid() == true);}

Page 76: Driving Design through Examples - PhpCon PL 2015

PHP Fatal error: Call to undefined method Ticket::isCompletelyPaid()

Page 77: Driving Design through Examples - PhpCon PL 2015

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(); }}

Page 78: Driving Design through Examples - PhpCon PL 2015

class Ticket{ private $fare;

// ...

public function pay(Fare $fare) { $this->fare = $this->fare->deduct($fare); }

public function isCompletelyPaid() { return $this->fare->isZero(); }}

Page 79: Driving Design through Examples - PhpCon PL 2015

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')); }}

Page 80: Driving Design through Examples - PhpCon PL 2015

class Fare{ private $pence;

private function __construct($pence) { $this->pence = $pence; }

// ...

public function deduct(Fare $amount) { return new Fare($this->pence - $amount->pence); }}

Page 81: Driving Design through Examples - PhpCon PL 2015

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(); }}

Page 82: Driving Design through Examples - PhpCon PL 2015

class Fare{ private $pence;

private function __construct($pence) { $this->pence = $pence; }

// ...

public function isZero() { return $this->pence == 0; }}

Page 83: Driving Design through Examples - PhpCon PL 2015

Run Behat

Page 84: Driving Design through Examples - PhpCon PL 2015

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'))); }}

Page 85: Driving Design through Examples - PhpCon PL 2015

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())); }}

Page 86: Driving Design through Examples - PhpCon PL 2015

interface FareList{ public function listFare(Route $route, Fare $fare);

public function findFareFor(Route $route);}

Page 87: Driving Design through Examples - PhpCon PL 2015

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()]; }}

Page 88: Driving Design through Examples - PhpCon PL 2015

Run Behat

Page 89: Driving Design through Examples - PhpCon PL 2015

/** * @Then I the ticket should be worth :points loyalty points */public function iTheTicketShouldBeWorthLoyaltyPoints(Points $points){ assert($this->ticket->getPoints() == $points);}

Page 90: Driving Design through Examples - PhpCon PL 2015

class FareSpec extends ObjectBehavior{ function let() { $this->beConstructedFromString('100.00'); }

// ...

function it_calculates_points() { $this->getPoints()->shouldBeLike(\Points::fromString('100')); }}

Page 91: Driving Design through Examples - PhpCon PL 2015

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')); }}

Page 92: Driving Design through Examples - PhpCon PL 2015

<?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(); }}

Page 93: Driving Design through Examples - PhpCon PL 2015

Run Behat

Page 94: Driving Design through Examples - PhpCon PL 2015

Where is our domain model?

Page 95: Driving Design through Examples - PhpCon PL 2015

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

Page 96: Driving Design through Examples - PhpCon PL 2015

> 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

Page 97: Driving Design through Examples - PhpCon PL 2015

UI Testing

Page 98: Driving Design through Examples - PhpCon PL 2015

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!

Page 99: Driving Design through Examples - PhpCon PL 2015

default: suites: core: contexts: [ FlightsContext ] web: contexts: [ WebFlightsContext ] filters: { tags: @ui }

Page 100: Driving Design through Examples - PhpCon PL 2015

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 ...

Page 101: Driving Design through Examples - PhpCon PL 2015

Modelling by Example→ Focuses attention on use cases

→ Helps developers understand core business domains

→ Encourages layered architecture→ Speeds up test suites

Page 102: Driving Design through Examples - PhpCon PL 2015

Use it when→ Project is core to your business

→ You are likely to support business changes in the future

→ You can have conversations with stakeholders

Page 103: Driving Design through Examples - PhpCon PL 2015

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)

Page 104: Driving Design through Examples - PhpCon PL 2015

Thank you!@ciaranmcnulty

Inviqa / Sensio Labs UK / Session Digital / iKOShttps://joind.in/16243

Questions?