Features
This chapter contains overview and examples of some Cucumber and Gherkin features.
Rule
keyword
The purpose of the Rule
keyword is to represent a business rule that should be implemented. It provides additional information for a feature. A Rule
is used to group together several scenarios that belong to this business rule. A Rule
should contain one or more scenarios that illustrate the particular rule.
You don't need additional work on the implementation side to support Rule
s. Let's take final example from Getting Started chapter and change the .feature
file to:
Feature: Animal feature
Rule: Hungry cat becomes satiated
Scenario: If we feed a hungry cat it will no longer be hungry
Given a hungry cat
When I feed the cat
Then the cat is not hungry
Rule: Satiated cat remains the same
Scenario: If we feed a satiated cat it will not become hungry
Given a satiated cat
When I feed the cat
Then the cat is not hungry
Background
keyword
Occasionally you’ll find yourself repeating the same Given
steps in all the scenarios of a Feature
.
Since it's repeated in every scenario, this is an indication that those steps are not essential to describe the scenarios, so they are incidental details. You can literally move such Given
steps to background, by grouping them under a Background
section.
Background
allows you to add some context to the Scenario
s following it. It can contain one or more steps, which are run before each scenario (but after any Before
hooks).
Feature: Animal feature
Background:
Given a hungry cat
Rule: Hungry cat becomes satiated
Scenario: If we feed a hungry cat it will no longer be hungry
When I feed the cat
Then the cat is not hungry
Rule: Satiated cat remains the same
Background:
When I feed the cat
Scenario: If we feed a satiated cat it will not become hungry
When I feed the cat
Then the cat is not hungry
Background
Step
s indicated by >
sign in the output by default.
In case Background
is declared outside any Rule
, it will be run on any Scenario
. Otherwise, if Background
is declared inside Rule
, it will be run only for Scenario
s inside this Rule
and only after top-level Background
statements, if any.
Tips for using Background
- Don’t use
Background
to set up complicated states, unless that state is actually something the client needs to know. - Keep your
Background
section short. - Make your
Background
section vivid, use colorful names, and try to tell a story. - Keep your
Scenario
s short, and don’t have too many.
Clearly, example provided above doesn't need Background
and was done for demonstration purposes only.
Scenario Outline
keyword
The Scenario Outline
keyword can be used to run the same Scenario
multiple times, with different combinations of values:
Feature: Animal feature
Scenario Outline: If we feed a hungry animal it will no longer be hungry
Given a hungry <animal>
When I feed the <animal>
Then the <animal> is not hungry
Examples:
| animal |
| cat |
| dog |
| 🦀 |
And leverage regex
support to match Step
s:
use std::{convert::Infallible, time::Duration}; use async_trait::async_trait; use cucumber::{given, then, when, World, WorldInit}; use tokio::time::sleep; #[derive(Debug)] struct Cat { pub hungry: bool, } impl Cat { fn feed(&mut self) { self.hungry = false; } } #[derive(Debug, WorldInit)] pub struct AnimalWorld { cat: Cat, } #[async_trait(?Send)] impl World for AnimalWorld { type Error = Infallible; async fn new() -> Result<Self, Infallible> { Ok(Self { cat: Cat { hungry: false }, }) } } #[given(regex = r"^a (hungry|satiated) (\S+)$")] async fn hungry_cat(world: &mut AnimalWorld, state: String) { sleep(Duration::from_secs(2)).await; match state.as_str() { "hungry" => world.cat.hungry = true, "satiated" => world.cat.hungry = false, _ => unreachable!(), } } #[when(regex = r"^I feed the (\S+)$")] async fn feed_cat(world: &mut AnimalWorld) { sleep(Duration::from_secs(2)).await; world.cat.feed(); } #[then(regex = r"^the (\S+) is not hungry$")] async fn cat_is_fed(world: &mut AnimalWorld) { sleep(Duration::from_secs(2)).await; assert!(!world.cat.hungry); } #[tokio::main] async fn main() { AnimalWorld::run("/tests/features/book/features/scenario_outline.feature").await; }
Combining regex
/cucumber-expressions
and FromStr
At parsing stage, <templates>
are replaced by value from cells. That means you can parse table cells into any type, that implements FromStr
.
Feature: Animal feature
Scenario Outline: If we feed a hungry animal it will no longer be hungry
Given a hungry <animal>
When I feed the <animal> <n> times
Then the <animal> is not hungry
Examples:
| animal | n |
| cat | 2 |
| dog | 3 |
| 🦀 | 4 |
use std::{convert::Infallible, str::FromStr, time::Duration}; use async_trait::async_trait; use cucumber::{given, then, when, Parameter, World, WorldInit}; use tokio::time::sleep; #[derive(Debug)] struct AnimalState { pub hungry: bool } impl AnimalState { fn feed(&mut self) { self.hungry = false; } } #[derive(Debug, WorldInit)] pub struct AnimalWorld { cat: AnimalState, dog: AnimalState, ferris: AnimalState, } #[async_trait(?Send)] impl World for AnimalWorld { type Error = Infallible; async fn new() -> Result<Self, Infallible> { Ok(Self { cat: AnimalState { hungry: false }, dog: AnimalState { hungry: false }, ferris: AnimalState { hungry: false }, }) } } #[derive(Clone, Copy)] enum State { Hungry, Satiated, } impl FromStr for State { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "hungry" => Ok(Self::Hungry), "satiated" => Ok(Self::Satiated), _ => Err("expected hungry or satiated"), } } } #[derive(Clone, Copy, Parameter)] #[param(regex = "cat|dog|🦀")] enum Animal { Cat, Dog, Ferris, } impl FromStr for Animal { type Err = &'static str; fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "cat" => Ok(Self::Cat), "dog" => Ok(Self::Dog), "🦀" => Ok(Self::Ferris), _ => Err("expected 'cat', 'dog' or '🦀'"), } } } #[given(regex = r"^a (hungry|satiated) (cat|dog|🦀)$")] async fn hungry_cat(world: &mut AnimalWorld, state: State, animal: Animal) { sleep(Duration::from_secs(2)).await; let hunger = match state { State::Hungry => true, State::Satiated => false, }; match animal { Animal::Cat => world.cat.hungry = hunger, Animal::Dog => world.dog.hungry = hunger, Animal::Ferris => world.ferris.hungry = hunger, }; } #[when(regex = r"^I feed the (cat|dog|🦀) (\d+) times?$")] async fn feed_cat(world: &mut AnimalWorld, animal: Animal, times: usize) { sleep(Duration::from_secs(2)).await; for _ in 0..times { match animal { Animal::Cat => world.cat.feed(), Animal::Dog => world.dog.feed(), Animal::Ferris => world.ferris.feed(), }; } } #[then(expr = "the {animal} is not hungry")] async fn cat_is_fed(world: &mut AnimalWorld, animal: Animal) { sleep(Duration::from_secs(2)).await; match animal { Animal::Cat => assert!(!world.cat.hungry), Animal::Dog => assert!(!world.dog.hungry), Animal::Ferris => assert!(!world.ferris.hungry), }; } #[tokio::main] async fn main() { AnimalWorld::run("/tests/features/book/features/scenario_outline_fromstr.feature").await; }
Spoken languages
The language you choose for Gherkin should be the same language your users and domain experts use when they talk about the domain. Translating between two languages should be avoided.
This is why Gherkin has been translated to over 70 languages.
A # language:
header on the first line of a .feature
file tells Cucumber which spoken language to use (for example, # language: fr
for French). If you omit this header, Cucumber will default to English (en
).
# language: no
Egenskap: Animal feature
Eksempel: If we feed a hungry cat it will no longer be hungry
Gitt a hungry cat
Når I feed the cat
Så the cat is not hungry
In case most of your .feature
files aren't written in English and you want to avoid endless # language:
comments, use Cucumber::language()
method to override the default language.
Scenario hooks
Before
hook
Before
hook runs before the first step of each scenario, even before Background
ones.
use std::{convert::Infallible, time::Duration}; use async_trait::async_trait; use cucumber::WorldInit; use futures::FutureExt as _; use tokio::time; #[derive(Debug, WorldInit)] struct World; #[async_trait(?Send)] impl cucumber::World for World { type Error = Infallible; async fn new() -> Result<Self, Self::Error> { Ok(World) } } fn main() { World::cucumber() .before(|_feature, _rule, _scenario, _world| { time::sleep(Duration::from_millis(10)).boxed_local() }) .run_and_exit("tests/features/book"); }
⚠️ Think twice before using
Before
hook!
Whatever happens in aBefore
hook is invisible to people reading.feature
s. You should consider using aBackground
as a more explicit alternative, especially if the setup should be readable by non-technical people. Only use aBefore
hook for low-level logic such as starting a browser or deleting data from a database.
After
hook
After
hook runs after the last step of each Scenario
, even when that step fails or is skipped.
use std::{convert::Infallible, time::Duration}; use async_trait::async_trait; use cucumber::WorldInit; use futures::FutureExt as _; use tokio::time; #[derive(Debug, WorldInit)] struct World; #[async_trait(?Send)] impl cucumber::World for World { type Error = Infallible; async fn new() -> Result<Self, Self::Error> { Ok(World) } } fn main() { World::cucumber() .after(|_feature, _rule, _scenario, _world| { time::sleep(Duration::from_millis(10)).boxed_local() }) .run_and_exit("tests/features/book"); }
CLI options
Library provides several options that can be passed to the command-line.
Use --help
flag to print out all the available options:
cargo test --test <test-name> -- --help
Default output is:
cucumber
Run the tests, pet a dog!
USAGE:
cucumber [FLAGS] [OPTIONS]
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
-v, --verbose Increased verbosity of an output: additionally outputs step's doc string (if present)
OPTIONS:
--color <auto|always|never> Coloring policy for a console output [default: auto]
-i, --input <glob> Glob pattern to look for feature files with. By default, looks for `*.feature`s
in the path configured tests runner
-c, --concurrency <int> Number of scenarios to run concurrently. If not specified, uses the value
configured in tests runner, or 64 by default
-n, --name <regex> Regex to filter scenarios by their name [aliases: scenario-name]
-t, --tags <tagexpr> Tag expression to filter scenarios by.
Note: Tags from Feature, Rule and Scenario are merged together on filtering,
so be careful about conflicting tags on different levels.
Example with tag expressions for filtering Scenario
s:
cargo test --test <test-name> -- --tags='@cat or @dog or @ferris'
Note: CLI overrides any configurations set in the code.
Customizing CLI options
CLI options are designed to be composable from the one provided by Parser::Cli
, Runner::Cli
and Writer::Cli
.
You may also extend CLI options with custom ones, if you have such a need for running your tests. See a cli::Opts
example for more details.
JUnit XML report
Library provides an ability to output tests result in as JUnit XML report.
Just enable output-junit
library feature in your Cargo.toml
:
cucumber = { version = "0.11", features = ["output-junit"] }
And configure Cucumber's output to writer::JUnit
:
use std::{convert::Infallible, fs, io}; use async_trait::async_trait; use cucumber::WorldInit; use cucumber::writer; #[derive(Debug, WorldInit)] struct World; #[async_trait(?Send)] impl cucumber::World for World { type Error = Infallible; async fn new() -> Result<Self, Self::Error> { Ok(World) } } #[tokio::main] async fn main() -> io::Result<()> { let file = fs::File::create(dbg!(format!("{}/target/junit.xml", env!("CARGO_MANIFEST_DIR"))))?; World::cucumber() .with_writer(writer::JUnit::new(file)) .run("tests/features/book") .await; Ok(()) }
Cucumber JSON format output
Library provides an ability to output tests result in a Cucumber JSON format.
Just enable output-json
library feature in your Cargo.toml
:
cucumber = { version = "0.11", features = ["output-json"] }
And configure Cucumber's output both to STDOUT and writer::Json
(with writer::Tee
):
use std::{convert::Infallible, fs, io}; use async_trait::async_trait; use cucumber::WorldInit; use cucumber::{writer, WriterExt as _}; #[derive(Debug, WorldInit)] struct World; #[async_trait(?Send)] impl cucumber::World for World { type Error = Infallible; async fn new() -> Result<Self, Self::Error> { Ok(World) } } #[tokio::main] async fn main() -> io::Result<()> { let file = fs::File::create(dbg!(format!("{}/target/schema.json", env!("CARGO_MANIFEST_DIR"))))?; World::cucumber() .with_writer( // `Writer`s pipeline is constructed in a reversed order. writer::Basic::stdout() // And output to STDOUT. .summarized() // Simultaneously, add execution summary. .tee::<World, _>(writer::Json::for_tee(file)) // Then, output to JSON file. .normalized() // First, normalize events order. ) .run_and_exit("tests/features/book") .await; Ok(()) }