|
| 1 | +Features |
| 2 | +======== |
| 3 | + |
| 4 | +This chapter contains overview and examples of some [Cucumber] and [Gherkin] features. |
| 5 | + |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +## `Rule` keyword |
| 10 | + |
| 11 | +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. |
| 12 | + |
| 13 | +You don't need additional work on the implementation side to support `Rule`s. Let's take final example from [Getting Started](Getting_Started.md) chapter and change the `.feature` file to: |
| 14 | + |
| 15 | +```gherkin |
| 16 | +Feature: Animal feature |
| 17 | + |
| 18 | + Rule: Hungry cat becomes satiated |
| 19 | + |
| 20 | + Scenario: If we feed a hungry cat it will no longer be hungry |
| 21 | + Given a hungry cat |
| 22 | + When I feed the cat |
| 23 | + Then the cat is not hungry |
| 24 | + |
| 25 | + Rule: Satiated cat remains the same |
| 26 | + |
| 27 | + Scenario: If we feed a satiated cat it will not become hungry |
| 28 | + Given a satiated cat |
| 29 | + When I feed the cat |
| 30 | + Then the cat is not hungry |
| 31 | +``` |
| 32 | + |
| 33 | +<script id="asciicast-9wOF9rEgGUgWN9e49TWiS5Nh3" src="https://asciinema.org/a/9wOF9rEgGUgWN9e49TWiS5Nh3.js" async data-autoplay="true" data-rows="18"></script> |
| 34 | + |
| 35 | + |
| 36 | + |
| 37 | + |
| 38 | +## `Background` keyword |
| 39 | + |
| 40 | +Occasionally you’ll find yourself repeating the same `Given` steps in all the scenarios of a `Feature`. |
| 41 | + |
| 42 | +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. |
| 43 | + |
| 44 | +```gherkin |
| 45 | +Feature: Animal feature |
| 46 | + |
| 47 | + Background: |
| 48 | + Given a hungry cat |
| 49 | + |
| 50 | + Rule: Hungry cat becomes satiated |
| 51 | + |
| 52 | + Scenario: If we feed a hungry cat it will no longer be hungry |
| 53 | + When I feed the cat |
| 54 | + Then the cat is not hungry |
| 55 | + |
| 56 | + Rule: Satiated cat remains the same |
| 57 | + |
| 58 | + Background: |
| 59 | + When I feed the cat |
| 60 | +
|
| 61 | + Scenario: If we feed a satiated cat it will not become hungry |
| 62 | + When I feed the cat |
| 63 | + Then the cat is not hungry |
| 64 | +``` |
| 65 | + |
| 66 | +<script id="asciicast-Q8OmAVWU116ZzxYg6VjBDxjlt" src="https://asciinema.org/a/Q8OmAVWU116ZzxYg6VjBDxjlt.js" async data-autoplay="true" data-rows="18"></script> |
| 67 | + |
| 68 | +`Background` `Step`s indicated by `>` sign in the output by default. |
| 69 | + |
| 70 | +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. |
| 71 | + |
| 72 | + |
| 73 | +### Tips for using `Background` |
| 74 | + |
| 75 | + - Don’t use `Background` to set up complicated states, unless that state is actually something the client needs to know. |
| 76 | + - Keep your `Background` section short. |
| 77 | + - Make your `Background` section vivid, use colorful names, and try to tell a story. |
| 78 | + - Keep your `Scenario`s short, and don’t have too many. |
| 79 | + |
| 80 | +Clearly, example provided above doesn't need `Background` and was done for demonstration purposes only. |
| 81 | + |
| 82 | + |
| 83 | + |
| 84 | + |
| 85 | +## `Scenario Outline` keyword |
| 86 | + |
| 87 | +The `Scenario Outline` keyword can be used to run the same `Scenario` multiple times, with different combinations of values: |
| 88 | + |
| 89 | +```gherkin |
| 90 | +Feature: Animal feature |
| 91 | +
|
| 92 | + Scenario Outline: If we feed a hungry animal it will no longer be hungry |
| 93 | + Given a hungry <animal> |
| 94 | + When I feed the <animal> |
| 95 | + Then the <animal> is not hungry |
| 96 | +
|
| 97 | + Examples: |
| 98 | + | animal | |
| 99 | + | cat | |
| 100 | + | dog | |
| 101 | + | 🦀 | |
| 102 | +``` |
| 103 | + |
| 104 | +And leverage `regex` support to match `Step`s: |
| 105 | + |
| 106 | +```rust |
| 107 | +# use std::{convert::Infallible, time::Duration}; |
| 108 | +# |
| 109 | +# use async_trait::async_trait; |
| 110 | +# use cucumber::{given, then, when, World, WorldInit}; |
| 111 | +# use tokio::time::sleep; |
| 112 | +# |
| 113 | +# #[derive(Debug)] |
| 114 | +# struct Cat { |
| 115 | +# pub hungry: bool, |
| 116 | +# } |
| 117 | +# |
| 118 | +# impl Cat { |
| 119 | +# fn feed(&mut self) { |
| 120 | +# self.hungry = false; |
| 121 | +# } |
| 122 | +# } |
| 123 | +# |
| 124 | +# #[derive(Debug, WorldInit)] |
| 125 | +# pub struct AnimalWorld { |
| 126 | +# cat: Cat, |
| 127 | +# } |
| 128 | +# |
| 129 | +# #[async_trait(?Send)] |
| 130 | +# impl World for AnimalWorld { |
| 131 | +# type Error = Infallible; |
| 132 | +# |
| 133 | +# async fn new() -> Result<Self, Infallible> { |
| 134 | +# Ok(Self { |
| 135 | +# cat: Cat { hungry: false }, |
| 136 | +# }) |
| 137 | +# } |
| 138 | +# } |
| 139 | +# |
| 140 | +#[given(regex = r"^a (hungry|satiated) (\S+)$")] |
| 141 | +async fn hungry_cat(world: &mut AnimalWorld, state: String) { |
| 142 | + sleep(Duration::from_secs(2)).await; |
| 143 | + |
| 144 | + match state.as_str() { |
| 145 | + "hungry" => world.cat.hungry = true, |
| 146 | + "satiated" => world.cat.hungry = false, |
| 147 | + _ => unreachable!(), |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +#[when(regex = r"^I feed the (\S+)$")] |
| 152 | +async fn feed_cat(world: &mut AnimalWorld) { |
| 153 | + sleep(Duration::from_secs(2)).await; |
| 154 | + |
| 155 | + world.cat.feed(); |
| 156 | +} |
| 157 | + |
| 158 | +#[then(regex = r"^the (\S+) is not hungry$")] |
| 159 | +async fn cat_is_fed(world: &mut AnimalWorld) { |
| 160 | + sleep(Duration::from_secs(2)).await; |
| 161 | + |
| 162 | + assert!(!world.cat.hungry); |
| 163 | +} |
| 164 | +# |
| 165 | +# #[tokio::main] |
| 166 | +# async fn main() { |
| 167 | +# AnimalWorld::run("/tests/features/book/features/scenario_outline.feature").await; |
| 168 | +# } |
| 169 | +``` |
| 170 | + |
| 171 | +<script id="asciicast-15ZcRGFBUXubvcle34ZOLiLtO" src="https://asciinema.org/a/15ZcRGFBUXubvcle34ZOLiLtO.js" async data-autoplay="true" data-rows="18"></script> |
| 172 | + |
| 173 | + |
| 174 | +### Combining `regex` and `FromStr` |
| 175 | + |
| 176 | +At parsing stage, `<templates>` are replaced by value from cells. That means you can parse table cells into any type, that implements [`FromStr`](https://doc.rust-lang.org/stable/std/str/trait.FromStr.html). |
| 177 | + |
| 178 | +```gherkin |
| 179 | +Feature: Animal feature |
| 180 | +
|
| 181 | + Scenario Outline: If we feed a hungry animal it will no longer be hungry |
| 182 | + Given a hungry <animal> |
| 183 | + When I feed the <animal> <n> times |
| 184 | + Then the <animal> is not hungry |
| 185 | +
|
| 186 | + Examples: |
| 187 | + | animal | n | |
| 188 | + | cat | 2 | |
| 189 | + | dog | 3 | |
| 190 | + | 🦀 | 4 | |
| 191 | +``` |
| 192 | + |
| 193 | +```rust |
| 194 | +# use std::{convert::Infallible, str::FromStr, time::Duration}; |
| 195 | +# |
| 196 | +# use async_trait::async_trait; |
| 197 | +# use cucumber::{given, then, when, World, WorldInit}; |
| 198 | +# use tokio::time::sleep; |
| 199 | +# |
| 200 | +# #[derive(Debug)] |
| 201 | +# struct Cat { |
| 202 | +# pub hungry: bool, |
| 203 | +# } |
| 204 | +# |
| 205 | +# impl Cat { |
| 206 | +# fn feed(&mut self) { |
| 207 | +# self.hungry = false; |
| 208 | +# } |
| 209 | +# } |
| 210 | +# |
| 211 | +# #[derive(Debug, WorldInit)] |
| 212 | +# pub struct AnimalWorld { |
| 213 | +# cat: Cat, |
| 214 | +# } |
| 215 | +# |
| 216 | +# #[async_trait(?Send)] |
| 217 | +# impl World for AnimalWorld { |
| 218 | +# type Error = Infallible; |
| 219 | +# |
| 220 | +# async fn new() -> Result<Self, Infallible> { |
| 221 | +# Ok(Self { |
| 222 | +# cat: Cat { hungry: false }, |
| 223 | +# }) |
| 224 | +# } |
| 225 | +# } |
| 226 | +# |
| 227 | +enum State { |
| 228 | + Hungry, |
| 229 | + Satiated, |
| 230 | +} |
| 231 | + |
| 232 | +impl FromStr for State { |
| 233 | + type Err = &'static str; |
| 234 | + |
| 235 | + fn from_str(s: &str) -> Result<Self, Self::Err> { |
| 236 | + match s { |
| 237 | + "hungry" => Ok(Self::Hungry), |
| 238 | + "satiated" => Ok(Self::Satiated), |
| 239 | + _ => Err("expected hungry or satiated"), |
| 240 | + } |
| 241 | + } |
| 242 | +} |
| 243 | + |
| 244 | +#[given(regex = r"^a (\S+) (\S+)$")] |
| 245 | +async fn hungry_cat(world: &mut AnimalWorld, state: State) { |
| 246 | + sleep(Duration::from_secs(2)).await; |
| 247 | + |
| 248 | + match state { |
| 249 | + State::Hungry => world.cat.hungry = true, |
| 250 | + State::Satiated => world.cat.hungry = false, |
| 251 | + } |
| 252 | +} |
| 253 | + |
| 254 | +#[when(regex = r"^I feed the (?:\S+) (\d+) times?$")] |
| 255 | +async fn feed_cat(world: &mut AnimalWorld, times: usize) { |
| 256 | + sleep(Duration::from_secs(2)).await; |
| 257 | + |
| 258 | + for _ in 0..times { |
| 259 | + world.cat.feed(); |
| 260 | + } |
| 261 | +} |
| 262 | + |
| 263 | +#[then(regex = r"^the (\S+) is not hungry$")] |
| 264 | +async fn cat_is_fed(world: &mut AnimalWorld) { |
| 265 | + sleep(Duration::from_secs(2)).await; |
| 266 | + |
| 267 | + assert!(!world.cat.hungry); |
| 268 | +} |
| 269 | +# |
| 270 | +# #[tokio::main] |
| 271 | +# async fn main() { |
| 272 | +# AnimalWorld::run("/tests/features/book/features/scenario_outline_fromstr.feature").await; |
| 273 | +# } |
| 274 | +``` |
| 275 | + |
| 276 | +<script id="asciicast-joMErjGUVegtXPJgL8fc5x6pt" src="https://asciinema.org/a/joMErjGUVegtXPJgL8fc5x6pt.js" async data-autoplay="true" data-rows="18"></script> |
| 277 | + |
| 278 | + |
| 279 | + |
| 280 | + |
| 281 | +## Spoken languages |
| 282 | + |
| 283 | +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. |
| 284 | + |
| 285 | +This is why [Gherkin] has been translated to over [70 languages](https://cucumber.io/docs/gherkin/languages). |
| 286 | + |
| 287 | +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`). |
| 288 | + |
| 289 | +```gherkin |
| 290 | +# language: no |
| 291 | + |
| 292 | +Egenskap: Animal feature |
| 293 | + |
| 294 | + Eksempel: If we feed a hungry cat it will no longer be hungry |
| 295 | + Gitt a hungry cat |
| 296 | + Når I feed the cat |
| 297 | + Så the cat is not hungry |
| 298 | +``` |
| 299 | + |
| 300 | +<script id="asciicast-sDt8aoo9ZVPZRgiTuy8pSNro2" src="https://asciinema.org/a/sDt8aoo9ZVPZRgiTuy8pSNro2.js" async data-autoplay="true" data-rows="18"></script> |
| 301 | + |
| 302 | +In case most of your `.feature` files aren't written in English and you want to avoid endless `# language:` comments, use [`Cucumber::language()`](https://docs.rs/cucumber/*/cucumber/struct.Cucumber.html#method.language) method to override the default language. |
| 303 | + |
| 304 | + |
| 305 | + |
| 306 | + |
| 307 | +[Cucumber]: https://cucumber.io |
| 308 | +[Gherkin]: https://cucumber.io/docs/gherkin |
0 commit comments