Terminal output

By default, cucumber crate outputs tests result to STDOUT. It provides some CLI options for configuring the output.

Verbosity

By default, cucumber crate tries to keep the output quite minimal, but its verbosity may be increased with -v CLI option.

Just specifying -v makes no difference, as it refers to the default verbosity level (no additional info).

Output World on failures (-vv)

Increasing verbosity level with -vv CLI option, makes the state of the World being printed at the moment of failure.

extern crate cucumber;
extern crate tokio;

use std::time::Duration;

use cucumber::{given, then, when, World};
use tokio::time::sleep;

#[derive(Debug, Default)]
struct Animal {
    pub hungry: bool,
}

#[derive(Debug, Default, World)]
pub struct AnimalWorld {
    cat: Animal,
}

#[given(regex = r"^a (hungry|satiated) cat$")]
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("I feed the cat")]
async fn feed_cat(_: &mut AnimalWorld) {}

#[then("the cat 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::cucumber()
        .run_and_exit("tests/features/book/output/terminal_verbose.feature")
        .await;
}

record

This is intended to help debugging failed tests.

Output doc strings (-vvv)

By default, outputting doc strings of steps is omitted. To include them into the output use -vvv CLI option:

Feature: Animal feature
    
  Scenario: If we feed a hungry cat it will no longer be hungry
    Given a hungry cat
      """
      A hungry cat called Felix is rescued from a Whiskas tin in a calamitous 
      mash-up of cat food brands.
      """
    When I feed the cat
    Then the cat is not hungry
extern crate cucumber;
extern crate tokio;

use std::time::Duration;

use cucumber::{given, then, when, World};
use tokio::time::sleep;

#[derive(Debug, Default)]
struct Animal {
    pub hungry: bool,
}

impl Animal {
    fn feed(&mut self) {
        self.hungry = false;
    }
}

#[derive(Debug, Default, World)]
pub struct AnimalWorld {
    cat: Animal,
}

#[given(regex = r"^a (hungry|satiated) cat$")]
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("I feed the cat")]
async fn feed_cat(world: &mut AnimalWorld) {
    sleep(Duration::from_secs(2)).await;

    world.cat.feed();
}

#[then("the cat 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::cucumber()
        .run_and_exit("tests/features/book/output/terminal_verbose.feature")
        .await;
}

record

Coloring

Coloring may be disabled by specifying --color CLI option:

extern crate cucumber;
extern crate tokio;

use std::time::Duration;

use cucumber::{given, then, when, World};
use tokio::time::sleep;

#[derive(Debug, Default)]
struct Animal {
    pub hungry: bool,
}

impl Animal {
    fn feed(&mut self) {
        self.hungry = false;
    }
}

#[derive(Debug, Default, World)]
pub struct AnimalWorld {
    cat: Animal,
}

#[given(regex = r"^a (hungry|satiated) cat$")]
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("I feed the cat")]
async fn feed_cat(world: &mut AnimalWorld) {
    sleep(Duration::from_secs(2)).await;

    world.cat.feed();
}

#[then("the cat 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::cucumber()
        .run_and_exit("tests/features/book/output/terminal.feature")
        .await;
}

record

NOTE: By default, cucumber crate automatically disables coloring for non-interactive terminals, so there is no need to specify --color CLI option explicitly on CI.

Debug printing and/or logging

Though cucumber crate doesn't capture any manual debug printing produced in a step matching function (such as dbg! or println! macros), it may be quite misleading to produce and use it for debugging purposes. The reason is simply because cucumber crate executes scenarios concurrently and normalizes their results before outputting, while any manual print is produced instantly at the moment of its step execution.

WARNING: Moreover, manual printing will very likely interfere with default interactive pretty-printing.

extern crate cucumber;
extern crate tokio;

use std::time::Duration;

use cucumber::{given, then, when, World};
use tokio::time::sleep;

#[derive(Debug, Default)]
struct Animal {
    pub hungry: bool,
}

impl Animal {
    fn feed(&mut self) {
        self.hungry = false;
    }
}

#[derive(Debug, Default, World)]
pub struct AnimalWorld {
    cat: Animal,
}

#[given(regex = r"^a (hungry|satiated) cat$")]
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("I feed the cat")]
async fn feed_cat(world: &mut AnimalWorld) {
    sleep(Duration::from_secs(2)).await;
    dbg!("here!");
    world.cat.feed();
}

#[then("the cat 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::cucumber()
        .run_and_exit("tests/features/book/output/terminal.feature")
        .await;
}

record

To achieve natural output for debugging, the following preparations are required:

  1. Setting .max_concurrent_scenarios() to 1 for executing all the scenarios sequentially.
  2. Creating writer::Basic::raw with Coloring::Never to avoid interactive pretty-printed output.
  3. Wrapping it into writer::AssertNormalized to assure cucumber about the output being normalized already (due to sequential execution).
extern crate cucumber;
extern crate tokio;

use std::{io, time::Duration};

use cucumber::{given, then, when, writer, World, WriterExt as _};
use tokio::time::sleep;

#[derive(Debug, Default)]
struct Animal {
    pub hungry: bool,
}

impl Animal {
    fn feed(&mut self) {
        self.hungry = false;
    }
}

#[derive(Debug, Default, World)]
pub struct AnimalWorld {
    cat: Animal,
}

#[given(regex = r"^a (hungry|satiated) cat$")]
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("I feed the cat")]
async fn feed_cat(world: &mut AnimalWorld) {
    sleep(Duration::from_secs(2)).await;
    dbg!("here!");    
    world.cat.feed();
}

#[then("the cat 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::cucumber()
        .max_concurrent_scenarios(1)
        .with_writer(
            writer::Basic::raw(io::stdout(), writer::Coloring::Never, 0)
                .summarized()
                .assert_normalized(),
        )
        .run_and_exit("tests/features/book/output/terminal.feature")
        .await;
}

record

NOTE: The custom print is still output before its step, because is printed during the step execution.

Much better option for debugging would be using tracing crate integration instead of dbg!/println! for doing logs.

extern crate cucumber;
extern crate tokio;
extern crate tracing;

use std::{
    sync::atomic::{AtomicUsize, Ordering},
    time::Duration,
};

use cucumber::{given, then, when, World as _};
use tokio::time;

#[derive(cucumber::World, Debug, Default)]
struct World;

#[given(regex = r"(\d+) secs?")]
#[when(regex = r"(\d+) secs?")]
#[then(regex = r"(\d+) secs?")]
async fn sleep(_: &mut World, secs: u64) {
    static ID: AtomicUsize = AtomicUsize::new(0);

    let id = ID.fetch_add(1, Ordering::Relaxed);

    tracing::info!("before {secs}s sleep: {id}");
    time::sleep(Duration::from_secs(secs)).await;
    tracing::info!("after {secs}s sleep: {id}");
}

#[tokio::main]
async fn main() {
    World::cucumber()
        .init_tracing()
        .run("tests/features/wait")
        .await;
}

record

Repeating failed and/or skipped steps

As a number of scenarios grows, it may become quite difficult to find failed/skipped ones in a large output. This issue may be mitigated by duplicating failed and/or skipped steps at the and of output via Cucumber::repeat_failed() and Cucumber::repeat_skipped() methods respectively.

extern crate cucumber;
extern crate tokio;

use std::{time::Duration};

use cucumber::{given, then, when, World};
use tokio::time::sleep;

#[derive(Debug, Default)]
struct Animal {
    pub hungry: bool,
}

impl Animal {
    fn feed(&mut self) {
        self.hungry = false;
    }
}

#[derive(Debug, Default, World)]
pub struct AnimalWorld {
    cat: Animal,
}

#[given(regex = r"^a (hungry|satiated) cat$")]
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("I feed the cat")]
async fn feed_cat(world: &mut AnimalWorld) {
    sleep(Duration::from_secs(2)).await;

    world.cat.feed();
}

#[then("the cat 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::cucumber()
        .repeat_failed()
        .run_and_exit("tests/features/book/output/terminal_repeat_failed.feature")
        .await;
}

record

extern crate cucumber;
extern crate tokio;

use std::{time::Duration};

use cucumber::{given, then, when, World};
use tokio::time::sleep;

#[derive(Debug, Default)]
struct Animal {
    pub hungry: bool,
}

impl Animal {
    fn feed(&mut self) {
        self.hungry = false;
    }
}

#[derive(Debug, Default, World)]
pub struct AnimalWorld {
    cat: Animal,
}

#[given(regex = r"^a (hungry|satiated) cat$")]
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("I feed the cat")]
async fn feed_cat(world: &mut AnimalWorld) {
    sleep(Duration::from_secs(2)).await;

    world.cat.feed();
}

#[then("the cat 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::cucumber()
        .repeat_skipped()
        .run_and_exit("tests/features/book/output/terminal_repeat_skipped.feature")
        .await;
}

record