BDD: Writing stories as Rust traits

I used Cucumber and Gauge in past development, and I'm making GitHub - ryo33/narrative to resolve what I struggled with those tools, and to redefine the BDD. I prefer Rust's syntax and type system and rust-analyzer experience, so I've decided to leverage them to achieve my goal, creating a most simple and painless BDD facility. Now that some examples are working, I'd like to get feedback from the Rust community.
As the title says, the main point is using Rust trait as the story format instead of Gherkin or Markdown, to achieve these goals:

  • Story-driven: Code respects story, not the other way around
  • Data-driven: Enabling stories to include structured data
  • No additional tooling: Eliminating the need for extra installation and learning
  • Leverage existing ecosystem: Rich experience with less implementation
  • Zero runtime cost: Stories are processed at compile time

The current status of development is finding a neat way to declare struct, enum, trait, and function coupled with an exact story (the solution in the README does not work).

These features are implemented and work now:

  • generate a trait for implementing the story
  • generate an async version trait of the story
  • run_all() and run_all_async() to run all steps in a story
  • step by step execution with run(&mut env) and run_async(&mut env).
  • get story metadata: title and trait ident
  • get step metadata: title, ident, and the list arguments
  • get step arg metadata: formatted value with {:?}, and serialized value with any serde serializer.
  • get spanned error or warning from rust-analyzer if:
    • a step argument is missing or unused
    • a type not defined locally within a story
use std::convert::Infallible;

#[narrative::story("My First Story")]
trait MyFirstStory {
    #[step("Hi, I'm a user")]
    fn as_a_user();
    #[step("I have an apple", count = 1)]
    fn have_one_apple(count: u32);
    #[step("I have {count} orages", count = 2)]
    fn have_two_oranges(count: u32);
    #[step("I should have {total} fruits", total = 3)]
    fn should_have_three_fruits(total: u32);
}

struct MyFirstStoryEnv {
    sum: u32,
}

impl MyFirstStory for MyFirstStoryEnv {
    type Error = Infallible;

    fn as_a_user(&mut self) -> Result<(), Self::Error> {
        Ok(())
    }
    fn have_one_apple(&mut self, count: u32) -> Result<(), Self::Error> {
        self.sum += count;
        Ok(())
    }
    fn have_two_oranges(&mut self, count: u32) -> Result<(), Self::Error> {
        self.sum += count;
        Ok(())
    }
    fn should_have_three_fruits(&mut self, total: u32) -> Result<(), Self::Error> {
        assert_eq!(self.sum, total);
        Ok(())
    }
}

#[test]
fn test() {
    let _ = MyFirstStoryEnv { sum: 0 }.run_all();
}

What is "BDD"?

It stands for Behavior-driven development. Behavior-driven development - Wikipedia

For making "software that matters."

1 Like

I have worked on plenty of software that matters. Where lives are in danger if it goes wrong. I don't immediately see anything in the BDD verbiage that would have improved the development processes I was involved in. It is likely the reason I am using Rust enthusiastically today though.

Though BDD may not be suitable for all team or Rust-made software, it still plays an important role in some aspects in software development, especially for teams like I've met that chose Rust for their crucial projects because they love Rust, rather than for safety.
If I've misunderstood the concept of "software that matters", please forget what I say, and i'd be happy if you two tell me that.

To clarify the intended use cases of narrative, we can use it for testing projects not written in Rust.

I don't think that you misunderstood "software that matters".

Personally I think any significant software effort matters. Not just safety critical systems. If it does not matter then why would one be doing it? Given that it matters it deserves the best we can do to ensure it does what it should and is as error free as possible. Part of the best we can do is to use a programming language that catches as many of our silly mistakes as possible. For example Rust.

It's not only for finding bugs, like asserting that the new feature we are going to start implementing does really help user?, or using the test as some kind of specification that is accessible to new member or not programmers.
I'd like to add that there are a lot of bug kinds, and rustc can catch a small part of them, while it has stronger ability to catch mistakes than other mainstream languages

I could be misreading this example, but two issues bother me:

  • It seems to force a relationship 1 story = 1 trait or type. This doesn't fit the way I think of stories.
  • The macro appears to add a &mut self arg to every method. This is often a poor choice.

Can you tell me what are situations that those cloud be problematic?

Constructors, methods meant to support simultaneous use by multiple threads, methods which should consume self (into).

I apologize for teasing.

2 Likes

I believe a story in this context is a sequence of steps, so I cannot imagine use cases that need constructors, parallelism, or taking ownership of the state.
In addition to the previous my post, the reason &mut self is implicit because I intended it to be written or read by also non-programmers.

Does that mean BDD is for creating tests and not the main code?

No I didn't mean that. We can use it for production code that can be abstract as a sequence of step.

Making signature consistent has strong effect in handling steps in general by using traits like this.

My answer was wrong, BDD is typically for writing tests, in my knowledge. But narrative can also be used for production code other than test code.

And that is a reason I use this expression "redefine BDD"

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.