The document discusses using examples and behavior-driven development (BDD) to drive the design of a domain model. Examples are presented in a formal language called Gherkin to illustrate behaviors like earning loyalty points for flights. The examples are then used to directly drive the code model, with behaviors tested first before user interfaces or infrastructure. By embedding the ubiquitous language from the domain in scenarios, the scenarios naturally become the domain model. Tests are written to define objects like flights, fares, tickets and points to match the examples.
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
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
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
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
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();
}
// ...
}
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();
}
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);
}
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()));
}
}
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!
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)