Essay : Extending BDD stories ; the "Given I did" clause - including flow in user stories

Introduction

In this essay I am going to reflect my personal ideas and opinions on the DSL syntax used for BDD, and I will propose a new concept for the common BDD language.

I am going to show you how I really grasped what I was doing wrong with my stories, and how the syntax should be extended in order to contain extra flow information in a really easy way.
This new concept adds extra conceptual information to the stories, and also reduces the work the developer has to do to implement the DSL.

Enjoy !

In the beginning, there was nothing !!!

I got started on this idea while I was replying to an email about the limitations of current BDD stories. Since my conversation partner said he had some issues on the current BDD syntax used, I asked him wether he could clarify on the issues.

While I was replying to the response he sent, I was trying to explain my thoughts by giving an example :

As a customer  
I want to place orders  
So that I can get the products where and when I want them

Scenario Place an order  
When I want to place an order  
Then I should be able to select products in a catalog  

Scenario Order products  
Given I have selected on or more products in a catalog  
When I add it to my shopping cart  
Then the shopping cart should contain the selected product  

Scenario Modify a product in the shopping cart  
Given I have some products in my shopping cart  
When I edit a product order  
Then the edited product order should have changed

Eureka !!

This was the moment I noticed that something was wrong. Let us take a good look at the last Scenario : “Modify a product in the shopping cart” . What is wrong with it, one might ask ? While I was writing this story I was thinking the following :

  • What if the shopping cart is empty ?
  • Hmmz I don’t know, I suppose we need another scenario for that
  • Another scenario for what ?
  • We define two scenarios : one for a shopping carts with products in it, and one for the case where it’s empty, and we add “Given the shopping cart is (not) empty”
  • But hey, why ? I’m not interested in the “Modify the shopping cart if the shopping cart is empty” because it is just litter, it doesn’t really add to the conceptual story.
  • Yeah, but as a developer I need to know what to do if it is empty and you try to modify it.
  • Are you stupid ?  Ok, maybe you need to say that it is not possible, but it adds no conceptual info at all to the story !!
  • Oh, you mean that it is actually some extra information on the scenario; the “shopping cart is not empty” is not just a given for this example; it is a requirement ???
  • Yes, it’s not a given, it is a requirement that should say that this story is only valid in this particular case
  • Hmmz interesting concept, let’s try :
Scenario Modify a product in the shopping cart  
If I have some products in my shopping cart  
When I edit an ordered product  
Then the edited product should have changed

- Whoa ! that looks nice; If we have this scenario, we do not need an extra scenario for the empty shopping cart  
- But wait, we could also say it like this

Scenario Modify a product in the shopping cart  
Given I have some products in my shopping cart  
When I edit an ordered product  
Then the edited product should have changed
  • What is the difference ?
  • Well, it is really a conceptual thing; I’ll try to think about another example
Scenario Place the order  
If I have identified myself as a valid customer  
When I edit an ordered product  
Then the edited product should have changed
  • Hmmz something is wrong with it ?
  • What ?

Eureka : Part deux

  • EUREKA !!!
  • What ? Again ???
  • I found it ; the “If” clause is actually describing something you did previously.
  • In the example the “If I have identified myself as a valid customer” actually means we should have some kind of a scenario called “Identify myself as a valid customer”
  • Oh, you are describing the flow here ?
  • Yups, that is it; it is extra information you are providing….
  • Wow, that is interesting !!
  • But, I see a small problem; the name of the scenario does not match the “If “clause
  • …. let’s see…. AHA !!
  • Yes ?
  • Here is the example :
Scenario Identify myself as a valid customer  
Given  I have some valid id method  
When  I identify myself  
Then   it should state me as a valid customer  

Scenario Place the order  
Given I did identify myself as a valid customer  
  And I did order products  
When place the order  
Then order should be confirmed  

Scenario Order products  
Given I did select one or more products in a catalog  
When I add it to my shopping cart  
Then the shopping cart should contain the selected product  
  • Whoa, that looks nice ! So what you are actually doing is extending the story the whole time ?
  • Yes, that’s it.
  • So we are gradually replacing all the “Given"s by the “Given I did” and thus including extra flow in the context ?
  • Yes !! And in theory we could go all the way until we get to the very bottom of every single implemented line of code…
  • Man ! So the developer only has to implement the “Given"s , and not the “Given I did” ?
  • Yups !!
  • That should save a lot of work !! nice !!

In conclusion

Using the “Given I did” clause, we can include extra conceptual information in the BDD stories; the stories become tighter, but they contain even more information then before.

By analyzing the “Given I did” specs, one can deduct the flow an application should allow. In the example stated above, the option of ordering with an empty shopping cart should not be available since it is not mentioned in the stories; the developer should only allow state transitions mentioned in the stories….

I truly think that we have something new here, and hope that this essay is a viable contribution to the BDD community on it’s own !!

Kind Regards,

Tom Janssens

PS : I’m not really schizophrenic; I was just looking for a way to reflect my thaughts ;)

Appendix A : a working example story :

Context      Be.Corebvba.Aubergine.Examples.Accounts.Contexts.AccountContext  
ContextDLL   Be.Corebvba.Aubergine.Examples.DLL  

Story Transfer money between accounts  

    As a user  
    I want to transfer money between accounts  
    So that I can have real use for my money  

    Given the balance of AccountA is 3m  
    Given the balance of AccountB is 2m  
    Given the owner of AccountA is the current user  

    Scenario Authenticate the current user as a 'valid or invalid' user  
        Given the name of the current user is 'username'  
          And the password of the current user is 'password'  
        When I request authentication for the current user  
        Then the process should 'fail or succeed'  

        Example  
            +--------------------+------------+------------+------------------+  
            | 'valid or invalid' | 'username' | 'password' | 'fail or succeed'|  
            +--------------------+------------+------------+------------------+  
            | valid              | Neo        | Red pill   | succeed          |  
            | invalid            | Neo        | Blue pill  | fail             |  
            +--------------------+------------+------------+------------------+  

    Scenario Authenticate the current user for 'an account'  
        Given I did authenticate the current user as a valid user  
        When I request authentication for 'an account' with the current user  
        Then the process should 'fail or succeed'  

        Example accounts :  
            +--------------+-------------------+  
            | 'an account' | 'fail or succeed' |  
            +--------------+-------------------+  
            |  AccountA    | succeed           |  
            |  AccountB    | fail              |  
            +--------------+-------------------+  

    Scenario Transfer AAm between 2 accounts  
        Given I did authenticate the current user for AccountA  
        When I transfer AAm from AccountA to AccountB with the current user  
        Then the balance of AccountA should be BBm   
        Then the balance of AccountB should be CCm   
        Then the process should 'fail or succeed'  

        Example for the transfer  

            +----+----+----+-------------------+  
            | AA | BB | CC | 'fail or succeed' |  
            +----+----+----+-------------------+  
            |  1 |  2 |  3 | succeed           |  
            |  2 |  1 |  4 | succeed           |  
            |  3 |  0 |  5 | succeed           |  
            |  4 |  3 |  2 | fail              |  
            +----+----+----+-------------------+  


### Appendix B : the resulting output of my runner:

Aubergine Console Runner - Core bvba - Tom Janssens 2009  

Processing file(s) : accounts\stories\*.txt  
  Processing file : C:\Projecten\Be.Corebvba.Aubergine\Be.Corebvba.Aubergine.ConsoleRunner\bin\Debug\accounts\stories\Transfer_money_between_accounts.txt  

    Story Transfer money between accounts =>OK  
        Given the balance of AccountA is 3m =>OK  
        Given the balance of AccountB is 2m =>OK  
        Given the owner of AccountA is the current user =>OK  
        Scenario Authenticate the current user as a valid user =>OK  
            Given the name of the current user is Neo =>OK  
            Given the password of the current user is Red pill =>OK  
            When I request authentication for the current user =>OK  
            Then the process should succeed =>OK  

        Scenario Authenticate the current user as a invalid user =>OK  
            Given the name of the current user is Neo =>OK  
            Given the password of the current user is Blue pill =>OK  
            When I request authentication for the current user =>OK  
            Then the process should fail =>OK  

        Scenario Authenticate the current user for AccountA =>OK  
            Given I did authenticate the current user as a valid user =>OK  
            When I request authentication for AccountA with the current user =>OK  
            Then the process should succeed =>OK  

        Scenario Authenticate the current user for AccountB =>OK  
            Given I did authenticate the current user as a valid user =>OK  
            When I request authentication for AccountB with the current user =>OK  
            Then the process should fail =>OK  

        Scenario Transfer 1m between 2 accounts =>OK  
            Given I did authenticate the current user for AccountA =>OK  
            When I transfer 1m from AccountA to AccountB with the current user =>OK  
            Then the balance of AccountA should be 2m =>OK  
            Then the balance of AccountB should be 3m =>OK  
            Then the process should succeed =>OK  

        Scenario Transfer 2m between 2 accounts =>OK  
            Given I did authenticate the current user for AccountA =>OK  
            When I transfer 2m from AccountA to AccountB with the current user =>OK  
            Then the balance of AccountA should be 1m =>OK  
            Then the balance of AccountB should be 4m =>OK  
            Then the process should succeed =>OK  

        Scenario Transfer 3m between 2 accounts =>OK  
            Given I did authenticate the current user for AccountA =>OK  
            When I transfer 3m from AccountA to AccountB with the current user =>OK  
            Then the balance of AccountA should be 0m =>OK  
            Then the balance of AccountB should be 5m =>OK  
            Then the process should succeed =>OK  

        Scenario Transfer 4m between 2 accounts =>OK  
            Given I did authenticate the current user for AccountA =>OK  
            When I transfer 4m from AccountA to AccountB with the current user =>OK  
            Then the balance of AccountA should be 3m =>OK  
            Then the balance of AccountB should be 2m =>OK  
            Then the process should fail =>OK

Appendix C : The context + DSL

    internal class

AccountContext  
    {  
        public User currentUser = new User();  
        public Account AccountA = new Account();  
        public Account AccountB = new Account();  
        public AccountService AccountService = new AccountService();  
        public ProcessStatus LastStatus;  

        [DSL(@"the (?<member>.+) of (?<instance>.+) is (?<value>.+)")]  
        void assignfield(object instance,string member,object value)  
        {  
            instance.Set(member, value);  
        }  

        [DSL(@"the (?<member>.+) of (?<instance>.+) should be (?<value>.*)")]  
        bool shouldbefield(object instance, string member, object value)  
        {  
            var obj = instance.Get<object>(member);  
            return Convert.ChangeType(value, obj.GetType()).Equals(obj);  
        }  

        [DSL(@"I request authentication for (?<user>.+)")]  
        void authenticate_for_account_x(User user)  
        {  
            LastStatus = AccountService.AuthenticateUser(user);  
        }  

        [DSL(@"I request authentication for (?<account>.+) with (?<user>.+)")]  
        void authenticate_for_account_x(User user, Account account)  
        {  
            LastStatus =  AccountService.AuthenticateUserForAccount(account,user);  
        }  

        [DSL(@"I transfer (?<amount>.+) from (?<from>.+) to (?<to>.+) with (?<user>.+)")]  
        void transfering_xm_from_a_to_b(decimal amount, Account from, Account to,User user)  
        {  
            LastStatus = AccountService.Transfer(user,amount, from,to);  
        }  

        [DSL]  
        bool The_process_should_fail()  
        {  
            return LastStatus.Success == false;  
        }  

        [DSL]  
        bool The_process_should_succeed()  
        {  
            return LastStatus.Success == true;  
        }  

        [DSL("(?<name>Account[AB])")]  
        Account getaccountAB(string name)  
        {  
            return this.Get<Account>(name);  
        }  

        [DSL(@"(?<amount>\d+)(?<ismillion>m?)")]  
        decimal getmillion(decimal amount,string  ismillon)  
        {  
            return amount*(ismillon=="m"?1m:1);  
        }  

        [DSL]  
        User the_current_user()  
        {  
            return currentUser;  
        }  
    }