expectation

Acceptance testing for Rust

Write the behaviour once.
Get the conflicts, the tests and the manual.

Describe your system as expectations in a shared vocabulary. Expectation checks that they agree, compiles your user stories into acceptance tests, runs them against the real thing, and writes the user manual, with a screenshot of every step.

Get started See a generated manual

$ cargo install --git https://github.com/ptescher/expectation expectation_cli
1 Specify
Expectation: Turning the dial up starts the kettle
  Given the kettle is off
  When the dial is above 55 %
  Then the kettle is heating

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

Every step is a declared state or event, so the suite is a model, not a script.

2 Check
error[conflict]: «The child lock holds the kettle»
and «Turning the dial up starts the kettle»
disagree about the kettle
 = 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» is the
   more specific; if it's an exception, add
   Overrides: Turning the dial up starts the kettle

Contradictions, unreachable behaviour and untested rules are caught before anything runs.

3 Test
Keeping little hands off
  arrange  the kettle is off, the child lock is off
  1. when   the user presses lock
       check  the child lock is on        ✓
  2. when   the dial is 100 %
       check  the kettle is off           ✓
  3. when   the dial is 80 %
       check  the kettle is heating       ✓
✓ Reaching «Running dry stops the kettle»

Stories become tests; the expectations supply the checks. Generated tests cover the rest.

4 Document

First cup

  1. Set the dial to 80 %.
    The kettle is heating.
The toy kettle heating, with its display reading HEATING

Stories are tutorials, features are topic pages, and the test evidence is one click away.

What you get

Conflicts, found mechanically

Conditions are sets of states, so overlaps and contradictions are exact. Exceptions are explicit with Overrides:, and they must form a DAG.

Stories that can't drift

A story that relies on unspecified behaviour, or contradicts an expectation, is rejected before it runs. It never has to restate what the rules already promise.

Coverage by construction

A flow DAG from the starting state gives every expectation a shortest route, and a generated test follows it when no story does.

Any system, through a driver

A dozen closures bind the vocabulary to your code: a library, a game loop, an API, a browser, a device. Time, polling and screenshots are built in.

Honest about gaps

What the driver or the machine can't do yet is "not supported yet", not a failure, and the manual shows it. Databases and containers start themselves.

A manual people read

Markdown for GitHub and an HTML site, in the user's words, with Say: and Shows: lines for what to press and what appears.

One suite, every front end

Embed a driver in the app (Swift, JavaScript or Rust) and the same runner drives it. The demo is one kettle built three times, held to one suite.

Embedded drivers in the guide →

Bind it, run it

Bind each term of the vocabulary to your system, then run the suite from a test. It fails with every broken promise, named after the expectation that made it.

Drivers in the guide →

let mut driver = Bindings::new(Kettle::default())
    .observe("the kettle", |k| k.phase_name())
    .set("the dial", |k, v| k.dial = v.as_f64().unwrap())
    .on("the user presses lock", Kettle::press_lock)
    .on_settle(|k| k.run_for(1.0))
    .on_screenshot(|k| Screenshot::svg(k.svg()));

Acceptance::load("expectations")?
    .with_coverage()
    .assert_passes(&mut driver, &RunOptions::default());
ReadThe guide Walk throughA worked example BrowseThe kettle manual CompareThree apps, one suite Look upThe API