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 Rules. 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 Scenarios 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 Steps 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 Scenarios inside this Rule and only after top-level Background statements, if any.
Tips for using Background
- Don’t use
Backgroundto set up complicated states, unless that state is actually something the client needs to know. - Keep your
Backgroundsection short. - Make your
Backgroundsection vivid, use colorful names, and try to tell a story. - Keep your
Scenarios 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 Steps:
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
Beforehook!
Whatever happens in aBeforehook is invisible to people reading.features. You should consider using aBackgroundas a more explicit alternative, especially if the setup should be readable by non-technical people. Only use aBeforehook 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 Scenarios:
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(()) }