Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

A worked example: the kettle

The kettle example is a suite of five files about an electric kettle, run against a toy kettle with time in it. This chapter walks through it: the vocabulary, a conflict and its exception, stories, generated coverage, a failing run, and the manual it writes.

cargo run -p expectation_harness --example kettle

The manual it writes is published here: the kettle manual.

The vocabulary

vocabulary.expect declares every word the suite may use, and where every test starts:

Vocabulary:
  State: the kettle is off | starting | heating | dry
  State: the dial is 0..100 %
  State: the preset is WARM | HOT | BOIL
  State: the child lock is on | off
  Event: the user presses start
  Event: the user presses reset
  Event: the user presses preset
  Event: the user presses lock
  Event: the water runs out

Background:
  Given the kettle is off
  And the dial is 0 %
  And the preset is WARM
  And the child lock is off

The dial is a number, so steps can compare it: above 55 %, between 20 and 40 %. The others have named values. “The water runs out” is an event without a person: the docs will say “wait until the water runs out” instead of telling the reader to do it.

Features and an exception

heating.expect says how the kettle starts and stops:

Feature: Heating
  The kettle starts off. Turn the dial past 55 % to start it, the way you would an old
  stovetop knob, or press start.

  Expectation: Turning the dial up starts the kettle
    Holding the dial above 55 % for a moment starts it.
    Given the kettle is off
    When the dial is above 55 %
    Then the kettle is heating
  ...

child_lock.expect adds a child lock, which keeps the dial from doing anything:

  Expectation: The child lock holds the kettle while the dial turns
    Given the kettle is off
    And the child lock is on
    When the dial is above 55 %
    Then the kettle is off

Both expectations apply when the kettle is off, the lock is on and the dial goes above 55 %, and they say opposite things. expectation check refuses the suite:

error[conflict]: «The child lock holds the kettle while the dial turns» and «Turning the dial up starts the kettle» disagree about the kettle
  --> child_lock.expect:15
   |   Expectation: The child lock holds the kettle while the dial turns
   = «The child lock holds the kettle while the dial turns» says the kettle is off
  --> heating.expect:5
   |   Expectation: Turning the dial up starts the kettle
   = «Turning the dial up starts the kettle» says the kettle is heating
   = note: both apply when the kettle is off and the child lock is on, and the dial is above 55 %
   = help: «The child lock holds the kettle while the dial turns» is the more specific; if it's an exception, add `Overrides: Turning the dial up starts the kettle` to it. Otherwise narrow a Given so they can't both apply

It is an exception, so the example says so:

  Expectation: The child lock holds the kettle while the dial turns
    Overrides: Turning the dial up starts the kettle
    Given the kettle is off
    And the child lock is on
    When the dial is above 55 %
    Then the kettle is off

Now the suite checks, and the docs for «Turning the dial up starts the kettle» carry a note: “Except when the child lock is on: see The child lock holds the kettle while the dial turns.”

Stories

stories.expect has three stories. Here’s one:

Story: Keeping little hands off
  As a parent
  I want the child lock to keep the kettle off
  So that a curious toddler turning the dial doesn't start it

  Scenario: Turn the dial with the lock on, then unlock and boil
    When the user presses lock
    Then the child lock is on
    When the dial is 100 %
    Then the kettle is off
    When the dial is 0 %
    And the user presses lock
    Then the child lock is off
    When the dial is 80 %
    Then the kettle is heating

expectation plan shows the test it compiles to. Every check comes from an expectation that fires at that step, so the story’s own Thens are only a statement of what the reader should see:

Keeping little hands off › Turn the dial with the lock on, then unlock and boil
  arrange  the kettle is off, the dial is 0 %, the preset is WARM and the child lock is off
  1. when   the user presses lock
       fires  «Lock turns the child lock on»
       check  the child lock is on  («Lock turns the child lock on»)
  2. when   the dial is 100 %
       fires  «The child lock holds the kettle while the dial turns»
       check  the kettle is off  («The child lock holds the kettle while the dial turns»)
  3. when   the dial is 0 % and the user presses lock
       fires  «Lock again turns it off»
       check  the child lock is off  («Lock again turns it off»)
  4. when   the dial is 80 %
       fires  «Turning the dial up starts the kettle»
       check  the kettle is heating  («Turning the dial up starts the kettle»)

A story can’t wander off the specification. Without the Overrides: line above, step 2 is an error too, because two expectations fire and disagree. A step where nothing is expected to happen is unspecified, and a Then that contradicts the expectations is contradicts.

Coverage

Four expectations have no story. check warns about each one, with a story that would reach it:

warning[untested]: no story exercises «Running dry stops the kettle»
  --> heating.expect:16
   |   Expectation: Running dry stops the kettle
   = help: a story that would: When the dial is above 55 % / When the water runs out

Acceptance::with_coverage() turns those routes into generated tests, so every expectation is verified even if no story mentions it:

✓ First cup › Turn it up, then turn it off
✓ Keeping little hands off › Turn the dial with the lock on, then unlock and boil
✓ Choosing a preset › Step to HOT and start
✓ Reaching «The child lock ignores the start button»
✓ Reaching «Running dry stops the kettle»
✓ Reaching «HOT to BOIL»
✓ Reaching «BOIL back to WARM»

The driver

world.rs is the toy kettle. Like a real one, it takes time: the dial has to stay above 55 % for 0.6 s before the kettle starts, and starting takes another 0.22 s. The driver binds each term and says how time passes:

#![allow(unused)]
fn main() {
Bindings::new(World::new(lock_leaks))
    .observe("the kettle", |w| w.phase_name())
    .observe("the dial", |w| w.dial)
    .set("the dial", |w, v| w.dial = v.as_f64().unwrap())
    .arrange("the kettle", |w, v| /* put it in that phase */)
    .on("the user presses lock", World::press_lock)
    // ...
    .on_settle(|w| (0..20).for_each(|_| w.tick(0.05)))   // a second after each action
    .on_advance(|w| { w.tick(0.1); Progress::Advanced }) // then 0.1 s per extra look
    .on_screenshot(|w| Screenshot::svg(w.svg()))
}

Settling a full second after each action is what makes “Then the kettle is off” mean it stays off, not that it was off for the instant before it switched on. See Drivers.

A failing run

--bug seeds a defect: the child lock lets the dial through.

cargo run -p expectation_harness --example kettle -- out --bug
✗ Keeping little hands off › Turn the dial with the lock on, then unlock and boil
    at: When the dial is 100 % Then the kettle is off
    expected the kettle is off («The child lock holds the kettle while the dial turns»); observed heating

The failure names the step, the check, the expectation that promised it, and what the kettle actually did. The steps after it are skipped, not failed. In the docs, that expectation is marked failed, and so is the tutorial.

The manual

The run writes a manual. The story became this tutorial, in the reader’s words:

# First cup

## Before you begin

- The kettle is off.
- The dial is 0 %.
- The preset is WARM.
- The child lock is off.

## Steps

1. **Set the dial to 80 %.**

   The kettle is heating.

   <screenshot>

2. **Set the dial to 0 %. Then press reset.**

   The kettle is off.

Each feature is a topic page, and the test evidence is collapsed under “How this is tested”. The vocabulary here has no Say:, Set: or Shows: lines, so the wording is worked out from the steps. With them, step 1 could read “Turn the dial past halfway. The red light comes on.” See Writing specs that read well.

Browse the result: the kettle manual.