Skip to content

Commit 2313010

Browse files
ilslvtyranron
andauthored
Add "Features" and "Test Modules Organization" chapters to the Book (#55, #64, #102, #103, #123, #132)
Additionally: - fix outputing English in writer::Basic instead of parsed keywords Co-authored-by: Kai Ren <tyranron@gmail.com>
1 parent a28ca00 commit 2313010

16 files changed

+685
-66
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ All user visible changes to `cucumber` crate will be documented in this file. Th
1616
- Renamed crate to `cucumber`.
1717
- Complete redesign: ([#128])
1818
- Introduce new abstractions: `Parser`, `Runner`, `Writer`;
19-
- Provide reference implementations for those abstractions.
19+
- Provide reference implementations for those abstractions;
20+
- Enable `macros` feature by default.
2021
- Replaced `#[given(step)]`, `#[when(step)]` and `#[then(step)]` function argument attributes with a single `#[step]`. ([#128])
22+
- Made test callbacks arguments consistent with proc macros: ([#128])
23+
- `&mut World` instead of `World` as a first parameter;
24+
- `Step` instead of `StepContext` as a second one.
25+
- CLI and [hooks](https://cucumber.io/docs/cucumber/api/#hooks) were removed, but are planned to be re-implemented with some changes in `0.11` release. ([#128])
2126

2227
### Added
2328

book/src/Features.md

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
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

book/src/Getting_Started.md

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Getting Started
1+
Getting Started
2+
===============
23

34
Adding [Cucumber] to your project requires some groundwork. [Cucumber] tests are run along with other tests via `cargo test`, but rely on `.feature` files corresponding to the given test, as well as a set of step matchers described in code corresponding to the steps in those `.feature` files.
45

@@ -100,52 +101,7 @@ There are 3 types of steps:
100101

101102
These various `Step` functions are executed to transform the `World`. As such, mutable reference to the world must always be passed in. The `Step` itself is also made available.
102103

103-
The steps matchers take a string, which is the name of the given `Step` (i.e., the literal string, such as `A hungry cat`), and then a function closure that takes a `World` and then the `Step` itself.
104-
105-
We also support regexes:
106-
```rust
107-
# use std::convert::Infallible;
108-
#
109-
# use async_trait::async_trait;
110-
# use cucumber::{given, World, WorldInit};
111-
#
112-
# #[derive(Debug)]
113-
# struct Cat {
114-
# pub hungry: bool,
115-
# }
116-
#
117-
# impl Cat {
118-
# fn feed(&mut self) {
119-
# self.hungry = false;
120-
# }
121-
# }
122-
#
123-
# #[derive(Debug, WorldInit)]
124-
# pub struct AnimalWorld {
125-
# cat: Cat,
126-
# }
127-
#
128-
# #[async_trait(?Send)]
129-
# impl World for AnimalWorld {
130-
# type Error = Infallible;
131-
#
132-
# async fn new() -> Result<Self, Infallible> {
133-
# Ok(Self {
134-
# cat: Cat { hungry: false },
135-
# })
136-
# }
137-
# }
138-
#
139-
#[given(regex = r"^a hungry (\S+)$")]
140-
fn hungry_someone(world: &mut AnimalWorld, who: String) {
141-
assert_eq!(who, "cat");
142-
world.cat.hungry = true;
143-
}
144-
#
145-
# fn main() {
146-
# futures::executor::block_on(AnimalWorld::run("/tests/features/book"));
147-
# }
148-
```
104+
The steps matchers take a string, which is the name of the given `Step` (i.e., the literal string, such as `A hungry cat`), and then a function closure that takes a `World` and then the `Step` itself.
149105

150106
We can add a `when` step after our `given` step:
151107
```rust

book/src/Multiple_Features.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

book/src/SUMMARY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
- [Introduction](Introduction.md)
44
- [Getting Started](Getting_Started.md)
5+
- [Features](Features.md)
6+
- [Test Modules Organization](Test_Modules_Organization.md)
57
- [Running Against a Process (WIP)](Running_Against_A_Process.md)
6-
- [Test Modules Organization (WIP)](Test_Modules_Organization.md)

0 commit comments

Comments
 (0)