Published a new Article on CodeProject : Getting started with BDD + new version of aubergine

by Tom 9. November 2009 06:39

Hi again,

I just published a brand new article on CodeProject : Getting started with BDD.

I also modified my Aubergine Framework a bit by adding a new feature : automatic case-insensitive DSL enum conversion.

You can download the latest version here (it also contains  the example for codeproject )

 

Aubergine_v0.04.zip (18,72 kb)

Enjoy !!

Edit

Since the article state on CodeProject is still "Pending", I simply posted it in the pages section of this blog; you can read it here.

Edit #2

v0.05 is released !!

Bookmark and Share

Aubergine .Net BDD : support for named/typed parameters + RECURSIVE DSL + bugfix + no more assertions

by Tom 7. November 2009 03:02

Ok, I had some ideas this morning when I woke up, so I quickly implemented them.


Changes (v0.02)

Here is the change_log for the new version :


    * Bugfix given a DSL attribute without a parameter is called
    * DSL definition changed to named parameters/typeconverters

Be.Corebvba.Aubergine.Examples_v0.02.zip (16,62 kb)

 

Edit : v0.03

Still going; next changelog :

    * Test Status changed from bool to bool? => null = test implementation error
    * No more need for debug.assert; just use "bool It_should_have(amount x) { return amount==x;}

So : no more assertions (ASSERTIONS ARE EVIL >:) ), and if a test fails because there is an implementation error, the status will now return a null value instead of false.

Be.Corebvba.Aubergine.Examplesv0.03.zip (16,87 kb)

 

Edit : v0.04

look here

 

Example

This has simplified the more complicated DSL definitions a LOT ; check out the new defintion for the AccountContext :


    internal class AccountContext
    {
        public Account AccountA = new Account();
        public Account AccountB = new Account();
        public Exception WhenException;

        #region Given

        [DSL(@"(?<account>Account[AB])_has_(?<amount>\d+)_m")]
        void accountX_has_Ym(Account account, decimal amount)
        {
            account.Balance = amount * 1m;
        }

        [DSL(@"the_current_user_is_authenticated_for_(?<account>Account[AB])")]
        void authenticate_for_account_x(Account account)
        {
            account.IsAuthenticated = true;
        }

        #endregion


        #region When

        [DSL(@"transfering_(?<amount>\d+)_m_from_(?<from>Account[AB])_to_(?<to>Account[AB])")]
        void transfering_xm_from_a_to_b(decimal amount, Account from, Account to)
        {
            from.Transfer(amount * 1m, to);
        }
        #endregion

        #region Then

        [DSL(@"it_should_have_(?<amount>\d+)_m_on_(?<account>Account[AB])")]
        bool should_have_Xm_on_AccountY(Account account, decimal amount)
        {
            return account.Balance == amount * 1m;
        }

        [DSL]
        bool it_should_fail_with_error()
        {
           return WhenException != null;
        }

        #endregion

        #region Recursive DSL

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

        #endregion
    }

Note the support for typed parameters and also DSL type converters. Due to the implication the expressiveness has changed a lot : you can now do recursive dsl definitions !!! I'll get in to this when I have more time, but really short : when you call a DSL function, the input string is pushed again to the interpreter. In theory you could define a complete language like this !!!

In the example above, [DSL(@"the_current_user_is_authenticated_for_(?<account>Account[AB])")] is called, and the result for the group &lt;account&gt; is agina pushed into the DSL engine; if a match is found, it is called, and the result of the function is returned; if not, it tries to do a Convert.ChangeyType(xxx,destintationtype);

finally for reference the Story as well as the output test results :


    class Transfer_money_between_accounts : Story<AccountContext>
    {
        As_a user;
        I_want to_transfer_money_between_accounts;
        So_that I_can_have_real_use_for_my_money;

        Given AccountA_has_3_m;
        Given AccountB_has_2_m;

        [Cols("xx", "yy", "zz")]
        [Data(1, 2, 3)]
        [Data(2, 1, 4)]
        [Data(3, 0, 5)]
        class Transfer_xx_m_between_2_accounts : Scenario
        {
            Given the_current_user_is_authenticated_for_AccountA;
            When transfering_xx_m_from_AccountA_to_AccountB;
            Then it_should_have_yy_m_on_AccountA;
            Then it_should_have_zz_m_on_AccountB;
        }

        class Transfer_too_much : Scenario
        {
            Given the_current_user_is_authenticated_for_AccountA;
            When transfering_4_m_from_AccountA_to_AccountB;
            Then it_should_have_3_m_on_AccountA;
            Then it_should_have_2_m_on_AccountB;
            Then it_should_fail_with_error;
        }

        class Not_authorized_for_transfer : Scenario
        {
            When transfering_1_m_from_AccountB_to_AccountA;
            Then it_should_have_3_m_on_AccountA;
            Then it_should_have_2_m_on_AccountB;
            Then it_should_fail_with_error;
        }
    }

output :

==STORY================================================================
Transfer_money_between_accounts => OK
========================================================================
   Transfer_1_m_between_2_accounts => OK
      Given AccountA_has_3_m => OK
      Given AccountB_has_2_m => OK
      Given the_current_user_is_authenticated_for_AccountA => OK
      When transfering_1_m_from_AccountA_to_AccountB => OK
      Then it_should_have_2_m_on_AccountA => OK
      Then it_should_have_3_m_on_AccountB => OK
   Transfer_2_m_between_2_accounts => OK
      Given AccountA_has_3_m => OK
      Given AccountB_has_2_m => OK
      Given the_current_user_is_authenticated_for_AccountA => OK
      When transfering_2_m_from_AccountA_to_AccountB => OK
      Then it_should_have_1_m_on_AccountA => OK
      Then it_should_have_4_m_on_AccountB => OK
   Transfer_3_m_between_2_accounts => OK
      Given AccountA_has_3_m => OK
      Given AccountB_has_2_m => OK
      Given the_current_user_is_authenticated_for_AccountA => OK
      When transfering_3_m_from_AccountA_to_AccountB => OK
      Then it_should_have_0_m_on_AccountA => OK
      Then it_should_have_5_m_on_AccountB => OK
   Transfer_too_much => OK
      Given AccountA_has_3_m => OK
      Given AccountB_has_2_m => OK
      Given the_current_user_is_authenticated_for_AccountA => OK
      When transfering_4_m_from_AccountA_to_AccountB => OK
      Then it_should_have_3_m_on_AccountA => OK
      Then it_should_have_2_m_on_AccountB => OK
      Then it_should_fail_with_error => OK
   Not_authorized_for_transfer => OK
      Given AccountA_has_3_m => OK
      Given AccountB_has_2_m => OK
      When transfering_1_m_from_AccountB_to_AccountA => OK
      Then it_should_have_3_m_on_AccountA => OK
      Then it_should_have_2_m_on_AccountB => OK
      Then it_should_fail_with_error => OK

 

 

Bookmark and Share

BDD with DSL: "Aubergine", a ruby/cucumber like alternative for .NET - download available

by Tom 6. November 2009 06:55

Update

Again a lot has changed : look here

 

 

Introduction

In this article you can download the very first alpha version of Aubergine; a BDD /DSL framework for .Net, initially based on Machine.Specifications, but later on heavily inspired by Cucumber. It is AFAIK the very first Cucumber like environment in .NET available, and you will see that it is very easy to use. Due to it's inspirator (Cucumber, I have decided to use the name Aubergine - I have no idea if they are actually related or not Undecided .

Please do note that it is an alpha version, so right now we only have a single testrunner that outputs to the console (i.e. no unit test integration yet). In the article I do include a postbuild step, which automatically makes my BDD tests run after each build, and displays the output in notepad, which is fine for me atm.

Anyway, enough with the talkin, Let's Get Busy !!!

 

Getting Started

First you need to download the example project; it includes all the binaries needed to do your BDD development. You can find it here :

Be.Corebvba.Aubergine.Examples.zip (16,43 kb)

Once you have the zip file, you can either explore the project, or walk through the following scenario to create your own test.

  1. Create a new .Net project in visual studio/any ide you use
  2. Add a reference to Be.Corebvba.Aubergine.DLL (can be found in the zip under the "lib" folder

 

 

Creating a story

Add a class named "BrowserContext" to your project

Add a class named "Make_sure_my_website_gets_enough_visibility" , import the Be.Corebvba.Aubergine namespace, and derive your class from Story<BrowserContext>

Then you can start typeing your story; the final file should look like this :


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Be.Corebvba.Aubergine;

namespace Example
{
    class Make_sure_my_website_gets_enough_visibility: Story&lt;BrowserContext&gt;
    {
        As_a website_owner;
        I_want to_make_sure_that_I_get_enough_visibility;
        So_that I_can_get_enough_traffic;

        [Cols("searchengine","search_url","keywords","my_url")]
        [Data("google","http://www.google.be/search?q=","core bvba tom janssens","www.corebvba.be")]
        [Data("google","http://www.google.be/search?q=","BDD .Net","www.corebvba.be")]
        [Data("bing","http://www.bing.com/search?q=","core bvba tom janssens","www.corebvba.be")]
        class Search_results_for_keywords_on_searchengine_should_contain_my_url : Scenario
        {
            Given current_url_is_search_url;
            When searching_for_keywords;
            Then result_should_contain_my_url;
        }
    }  
}

 

Define the context class

After having created a story with scenarios we need to define the context for these scenarios.

You add all your domain objects that need to be tested to the "BrowserContext" class

In this case it is quite simple :


    public class BrowserContext
    {
        public string Url { get; set; }
        public string Result { get; set; }

        private WebClient wc = new WebClient();
    }
          

This should be enough to test, but wait, isn't there something missing ?

 

The context DSL

Next up we need to define how to interpret the story. As I allready mentioned; this is heavily inspired by Ruby/Cucumber : regular expressions are used to find out how all scenario steps should be matched to a real function. While this maybe sounds complicated, it really isn't; this is the code :


    public class BrowserContext
    {
        public string Url { get; set; }
        public string Result { get; set; }

        private WebClient wc = new WebClient();
           
        [DSL("current_url_is_(.*)")]
        void SetUrl(string url)
        {
            Url = url;
        }

        [DSL("searching_for_(.*)")]
        void SearchForKeyWords(string keywords)
        {
            Result = wc.DownloadString(Url + HttpUtility.UrlEncode(keywords));
        }

        [DSL("result_should_contain_(.*)")]
        void ResultShouldContain(string myurl)
        {
            Result.Contains(myurl).ShouldEqual(true);
        }

    }




That's all there is to it; you are ready to run your tests now !!!

 

Running the specs

Ok, since we do not want to run these tests manually each time, but at every build action, we need to add a postbuildstep.

In visual studio you can do it like this :

  1. Menu : Project/Properties
  2. Tab build events
  3. put the following text in the post-build-event commandline :

 

"$(ProjectDir)\lib\Be.Corebvba.Aubergine.ConsoleRunner.exe" "$(TargetPath)" > "$(TargetDir)output.txt"
"$(TargetDir)output.txt"
exit 0 

 

Now build your project, and the tests will be ran !! Your default texteditor will start, and it should contain this text :

==STORY================================================================
Make_sure_my_website_gets_enough_visibility => OK
========================================================================
   Search_results_for_core bvba tom janssens_on_google_should_contain_www.corebvba.be => OK
      Given current_url_is_http://www.google.be/search?q= => OK
      When searching_for_core bvba tom janssens => OK
      Then result_should_contain_www.corebvba.be => OK
   Search_results_for_core bvba tom janssens_on_bing_should_contain_www.corebvba.be => OK
      Given current_url_is_http://www.bing.com/search?q= => OK
      When searching_for_core bvba tom janssens => OK
      Then result_should_contain_www.corebvba.be => OK
   Search_results_for_BDD .Net_on_google_should_contain_www.corebvba.be => OK
      Given current_url_is_http://www.google.be/search?q= => OK
      When searching_for_BDD .Net => OK
      Then result_should_contain_www.corebvba.be => OK

 

So how does this work ?

It's actually quite easy once you have the hang of it; this is pseudocode :

 

   foreach (var story in AllClassesDerivedFromTheAubergineStoryClass) 
      foreach(var scenario in AllPossibleScenariosFor(story))
         create a new context object
         foreach (var possiblesteps in AllSteps (given,when,then) in story and scenario)
            find a regex DSL match for possiblesteps.name in the context, extract all the regex groups and add them as string parameters to the function you call
            call the corresponding step function

 

If one of this steps should fail then the test fails and the reason is mentioned in the report.

If you expect a step to fail, then you should add a membervariable named "&lt;steptype&gt;Exception", and if a step of that type fails, the step is marked as successfull, but the Exceptionvariable will contain the exception thrown. You can see an example of this in the Example zip file.

Bookmark and Share

A new BDD framework in .Net : Aubergine

by Tom 4. November 2009 12:28

REMARK : An executable testrunner and some sample code is available

Here : http://www.corebvba.be/blog/post/BDD-with-DSL-Aubergine-a-rubycucumber-like-alternative-for-NET-download-available.aspx

REMARK : Skip to "Edit 2" to see the current syntax

 

Just a quick update !!

After thinking a bit more about the problems mentioned by Aaron in my previous post, and taking a look at Cucumber (in ruby), I decided to give it another go. Since I have to be in the shop within half an hour 20 mins I can only give you a sample of what the current spec looks like; explanations will come in later posts..

I'm also considering a rename, since netspec already seems to exist as a .Net spec platform... Since Cucumber was my inspiration, I'm currently thinking about Aubergine/Eggplant. Suggestions are welcome

I'm not really fond of the implementation yet, but I'll have to think about that a little bit more... As you might notice there has been a slight shift towards the Cucumber method of doing things.

Here we go :


    class TransferFundsBetweenAccounts : Story
    {
        AsAn AccountUser;
        IWant ToTransferMoneyBetweenAccounts;
        SoThat ICanHaveRealUseForMyMoney;

        class Transfer1MBetweenTwoAccounts : Scenario&lt;Context&gt;
        {
            Given AccountAHas1M;
            Given AccountBHas1M;
            When Transfering1MFromAccountAToAccountB;
            Then ShouldHave0MOnAccountA;
            Then ShouldHave2MOnAccountB;
        }

        class TransferTooMuch : Scenario&lt;Context&gt;
        {
            Given AccountAHas1M;
            Given AccountBHas1M;
            When Transfering3MFromAccountAToAccountB;
            Then ShouldHave1MOnAccountA;
            Then ShouldHave1MOnAccountB;
            Then ShouldFailWithError;
        }

        class Context
        {
            public Account AccountA = new Account();
            public Account AccountB = new Account();
        }

        class Contextimpl : ContextImplementation&lt;Context&gt;
        {

            public bool Get(string name,Context ctx)
            {
                return
                    Implement(@"(Account[AB])Has(\d+)M", (c, e) =&gt;
                        {
                            e.ByName&lt;Account&gt;(0,c).Balance = e.Get&lt;int&gt;(1) * 1m;
                        }).Run(name, ctx) &&
                    Implement(@"ShouldHave(\d+)MOn(Account[AB])", (c, e) =&gt;
                        {
                            e.ByName&lt;Account&gt;(1,c).Balance.ShouldEqual(e.Get&lt;int&gt;(0) * 1m);
                        }).Run(name, ctx) &&
                    Implement(@"Transfering(\d+)MFrom(Account[AB])To(Account[AB])", (c, e) =&gt;
                        {
                            e.ByName&lt;Account&gt;(1,c).Transfer(e.Get&lt;decimal&gt;(0) * 1m, e.ByName&lt;Account&gt;(2,c));
                        }).Run(name, ctx);
            }
        }
    }

As you might notice the biggest advantage is that multiple test scenarios can now be implemented since we have created some kind of a context-specific DSL... I'm not very fond of the way you Specify the DSL however; I'll probably need to rethink it.

As always I'll keep you posted on any progress I make.

 

Edit

After having dinner and thinking a bit about this, i got another eureka moment; this is what the code now looks like (Yes Aaron, you were also right about the box_case/PascalCase thingy ;-) ):


    class Transfer_money_between_accounts : Story
    {
        As_a user;
        I_want to_transfer_money_between_accounts;
        So_that I_can_have_real_use_for_my_money;


        Given&lt;Context&gt; AccountA_has_1m;
        Given&lt;Context&gt; AccountB_has_1m;
       
        class Transfer_1m_between_2_accounts : Scenario&lt;Context&gt;
        {
            Given the_current_user_is_authenticated_for_AccountA;
            When transfering_1m_from_AccountA_to_AccountB;
            Then should_have_0m_on_AccountA;
            Then should_have_2m_on_AccountB;
        }

        class Transfer_too_much : Scenario&lt;Context&gt;
        {
            Given the_current_user_is_authenticated_for_AccountA;
            When transfering_2m_from_AccountA_to_AccountB;
            Then should_have_1m_on_AccountA;
            Then should_have_1m_on_AccountB;
            Then should_fail_with_error;
        }

        class Not_authorized_for_transfer : Scenario&lt;Context&gt;
        {
            When transfering_1m_from_AccountB_to_AccountA;
            Then should_have_1m_on_AccountA;
            Then should_have_1m_on_AccountB;
            Then should_fail_with_error;
        }

        class Context
        {
            public Account AccountA = new Account();
            public Account AccountB = new Account();
        }

        public Dsl_for&lt;Context&gt; Dsl_for_Context = new Dsl_for&lt;Context&gt;();

        public Transfer_money_between_accounts()
        {
                Dsl_for_Context[@"(Account[AB])_has_(\d+)m"] = (c, e) =&gt;
                    c.Get&lt;Account&gt;(e[0]).Balance = e[1].As&lt;decimal&gt;() * 1m;

                Dsl_for_Context[@"should_have_(\d+)m_on_(Account[AB])"] = (c, e) =&gt;
                   c.Get&lt;Account&gt;(e[1]).Balance.ShouldEqual(e[0].As&lt;decimal&gt;() * 1m);

                Dsl_for_Context[@"transfering_(\d+)m_from_(Account[AB])_to_(Account[AB])"] = (c, e) =&gt;
                   c.Get&lt;Account&gt;(e[1]).Transfer(e[0].As&lt;decimal&gt;() * 1m, c.Get&lt;Account&gt;(e[2]));

                Dsl_for_Context[@"the_current_user_is_authenticated_for_(Account[AB])"] = (c, e) =&gt;
                   c.Get&lt;Account&gt;(e[0]).IsAuthenticated = true;
        }
    }

I have extended the story a bit in order to show that you can use story-wide givens as well.
Please do note that one can still assign the delegates as in MSpec, i.e. adding your code inline to the scenarios.
The DSL definition syntax looks a bit quirky, but it works like this :

  • "c" is an instance of the contextobject
  • "e" is an array of strings containing the regex matched subgroups
    An example : "e[1]" would return "AccountA" or "AccountB" in the case of "should_have_(\d+)m_on_(Account[AB])"
  • "c.Get&lt;Account&gt;(e[1])" gets the field- or propertyvalue from the context object "c" with the name found in e[1] and casts it to an "Account"

Also note that this is starting to look a lot like cucumber (which is a good thing, i think).

Again, another step closer to our destination and still going... Any reflections on this representation are welcome !!

Edit 2

Again, I have taken it a step further; I must say it is looking pretty acceptable now. The spec class is now separated from the contextclass, and the dsl is implemented in the contextclass.

First we have the Story in itself:


    class Transfer_money_between_accounts : Story&lt;AccountContext&gt;
    {
        As_a user;
        I_want to_transfer_money_between_accounts;
        So_that I_can_have_real_use_for_my_money;

        Given AccountA_has_3_m;
        Given AccountB_has_2_m;

        [Cols("xx","yy","zz")]
        [Data(0, 3, 2)]
        [Data(1, 2, 3)]
        [Data(2, 1, 4)]
        [Data(3, 0, 5)]
        class Transfer_xx_m_between_2_accounts : Scenario
        {
            Given the_current_user_is_authenticated_for_AccountA;
            When transfering_xx_m_from_AccountA_to_AccountB;
            Then should_have_yy_m_on_AccountA;
            Then should_have_zz_m_on_AccountB;
        }

        class Transfer_too_much : Scenario
        {
            Given the_current_user_is_authenticated_for_AccountA;
            When transfering_4_m_from_AccountA_to_AccountB;
            Then should_have_3_m_on_AccountA;
            Then should_have_2_m_on_AccountB;
            Then should_fail_with_error;
        }

        class Not_authorized_for_transfer : Scenario
        {
            When transfering_1_m_from_AccountB_to_AccountA;
            Then should_have_2_m_on_AccountA;
            Then should_have_3_m_on_AccountB;
            Then should_fail_with_error;
        }
    }

Which is perfectly understandable and should be maintainable by a domain expert.

Next we have the DSL/Context implementation which looks like this (Thank you cucumber) :


    class AccountContext
    {
        public Account AccountA = new Account();
        public Account AccountB = new Account();

        [DSL(@"(Account[AB])_has_(\d+)_m")]
        void accountX_has_Ym(string x, string y)
        {
            this.Get&lt;Account&gt;(x).Balance = y.As&lt;decimal&gt;()*1m;
        }

        [DSL(@"should_have_(\d+)_m_on_(Account[AB])")]
        void should_have_Xm_on_AccountY(string x, string y)
        {
            this.Get&lt;Account&gt;(y).Balance.ShouldEqual(x.As&lt;decimal&gt;() * 1m);
        }

        [DSL(@"transfering_(\d+)_m_from_(Account[AB])_to_(Account[AB])")]
        void transfering_xm_from_a_to_b(string x,string a,string b)
        {
           this.Get&lt;Account&gt;(a).Transfer(x.As&lt;decimal&gt;() * 1m, this.Get&lt;Account&gt;(b));
        }

        [DSL(@"the_current_user_is_authenticated_for_(Account[AB])")]
        void authenticate_for_account_x(string x)
        {
          this.Get&lt;Account&gt;(x).IsAuthenticated = true;
        }


        [DSL]
        void should_fail_with_error()
        {
            //TODO
        }
    }

I think that with this approach we might be getting close to the best possible BDD syntax in .Net.
We have the story for the domain expert, and the implementation of the context class can be left to the programmer.

Please let me know what you think.


Edit #3

I also added support for Example/Tables so that one can test the same scenario with different values.


Edit #4

It just occured to me: another advantage of the Example extension is that one can include special characters in the test_data. An example :

 


class Make_sure_my_website_gets_enough_visibility: Story&lt;BrowserContext&gt
{
    As_a website_owner;
    I_want to_make_sure_that_I_get_enough_visibility;
    So_that I_can_get_enough_traffic;

    [Cols("searchengine","search_url","keywords","my_url")]
    [Data("google","http://www.google.be/search?q=","core bvba tom janssens","www.corebvba.be")]
    [Data("google","http://www.google.be/search?q=","BDD .Net","www.corebvba.be")]
    [Data("bing","http://www.bing.com/search?q=","core bvba tom janssens","www.corebvba.be")]
    class Search_results_for_keywords_on_searchengine_should_contain_my_url : Scenario
    {
        Given current_url_is_search_url;
        When searching_for_keywords;
        Then result_should_contain_my_url;
    }
}

 

Which makes your testing scenarios even better. After running these tests you should get an output like this :


Make sure my website gets enough visibility => NOK
   As a website owner
   I want to make sure that I get enough visibility
   So that I can get enough traffic

   Search results for "core bvba tom janssens" on "google" should contain "www.corebvba.be" => OK
      Given current url is "http://www.google.be/search?q="
      When searching for "core bvba tom janssens"
      Then result should contain "www.corebvba.be"

   Search results for "BDD .net" on "google" should contain "www.corebvba.be" => NOK
      Given current url is "http://www.google.be/search?q="
      When searching for "BDD .Net"
      Then result should contain "www.corebvba.be"

   Search results for "core bvba tom janssens" on "bing" should contain "www.corebvba.be" => OK
     Given current url is "http://www.bing.com/search?q=" 
     When searching for "core bvba tom janssens"
     Then result should contain "www.corebvba.be"

 

Which is pretty neat I think !!!

Finally, this is the implementation of the BrowserContext class :


    public class BrowserContext
    {
        public string Url { get; set; }
        public string Result { get; set; }

        private WebClient wc = new WebClient();
           
        [DSL("current_url_is_(.*)")]
        void SetUrl(string url)
        {
            Url = url;
        }

        [DSL("searching_for_(.*)")]
        void SearchForKeyWords(string keywords)
        {
            Result = wc.DownloadString(Url + HttpUtility.UrlEncode(keywords));
        }

        [DSL("result_should_contain_(.*)")]
        void ResultShouldContain(string myurl)
        {
            Result.Contains(myurl).ShouldEqual(true);
        }
    }

Bookmark and Share

A new BDD framework in .Net : Aubergine

by Tom 3. November 2009 08:02

Update !! this has changed a lot

 

Check this post !!!

 

 

 

Introduction

During my exploration of BDD frameworks for .NET  I only had one final runner-up as a BDD framework : Machine.Specifications . This is a very nice framework, but in my quest for the holy grail on BDD it got me started on thinking about an even better BDD framework. In this article I will layout my initial ideas about a BDD framework named Aubergine.

 

Likes and dislikes about MSpec

MSpec is in my personal opinion one of the best BDD frameworks available for .Net, due to the very nice syntax.

An example :

First I'll show you the account class :


    public class Account
    {
        public Decimal Balance {get;set;}
        public void Transfer(decimal amount,Account ToAccount)
        {
            if (Balance < amount)
            {
                throw new ArgumentOutOfRangeException("There is not enough money available on the account");
            }
            Balance-=amount;
            ToAccount.Balance+=amount;
        }
    }

Next up I will show you the specs in Mspec :

using System;
 
namespace Machine.Specifications.Example
{
[Subject(typeof(Account), "Funds transfer")]
  public class when_transferring_between_two_accounts
    : with_from_account_and_to_account
  {
    Because of = () =>
      fromAccount.Transfer(1m, toAccount);
 
    It should_debit_the_from_account_by_the_amount_transferred = () =>
      fromAccount.Balance.ShouldEqual(0m);
 
    It should_credit_the_to_account_by_the_amount_transferred = () =>
      toAccount.Balance.ShouldEqual(2m);
  }
 
[Subject(typeof(Account), "Funds transfer"), Tags("failure")]
  public class when_transferring_an_amount_larger_than_the_balance_of_the_from_account
    : with_from_account_and_to_account
  {
    static Exception exception;
    Because of =()=>
      exception = Catch.Exception(()=>fromAccount.Transfer(2m, toAccount));
 
    It should_not_allow_the_transfer =()=>
      exception.ShouldBeOfType<Exception>();
  }
 
  public class failure {}
 
  public abstract class with_from_account_and_to_account
  {
    protected static Account fromAccount;
    protected static Account toAccount;
 
    Establish context =()=>
    {
      fromAccount = new Account {Balance = 1m};
      toAccount = new Account {Balance = 1m};
    };
  }
}

While this is a very keen and intelligent approach there are a few things that I am not very fond of :

  • The "Subject" attribute : is really code litter, and not really acceptable IMHO
  • The fact that we use classes and subclasses for each scenario; sometimes this becomes too much of a hassle; your code gets split up into several places, and this is not very transparent anymore...
  • The lack of Story support
  • The catching of expected exceptions should not need a try catch block but have some kind of other mechanism.
    Edit: My bad;there is a mechanism in MSpec... However, I'm not a real fan of it, since it is in the because part. IMHO this should be an "it", not a "because"
  • The fact that the context is intermixed with the class itself => this might be a subject of discussion, but it is certainly not my personal preference; I prefer a seperate context class per story and/or scenario
  • Some definitions, i.e. "because of", "Establish","It" etc... are not good enough in my opinion
  • The use of underscores; I am more of a PascalCasing fan
    Edit: After the remark of Aaron, I decided to implement both

Based on my ideas I created a new BDD Framework : NetSpec

 

The Aubergine approach

Based on my findings I started coding this morning, and this is the spec defined in NetSpec :


    class TransferFundsBetweenAccounts : Story
    {
        AsAn AccountUser;
        IWant ToTransferMoneyBetweenAccounts;
        SoThat ICanHaveRealUseForMyMoney;

        class Transfer1MBetweenTwoAccounts : Scenario&lt;Context&gt;
        {
            Given BothAccountsHave1M = () => new Context(1m,1m);
            When Transfering1MFromAToB = c => c.AccountA.Transfer(1m,c.AccountB);
            ItShould Have0mOnAccountA = c => c.AccountA.Balance.ShouldEqual(0m);
            ItShould Have2mOnAccountB = c => c.AccountB.Balance.ShouldEqual(2m);
        }

        class TransferTooMuch : Scenario&lt;Context&gt;
        {
            Given BothAccountsHave1M = () => new Context(1m, 1m);
            When Transfering2MFromAToB = c => c.AccountA.Transfer(2m,c.AccountB);
            ItShould Have1mOnAccountA = c => c.AccountA.Balance.ShouldEqual(1m);
            ItShould Have1mOnAccountB = c => c.AccountB.Balance.ShouldEqual(1m);
            ItShouldFailWithError AmountToLarge = e =>
                   e.ShouldBeOfType&lt;ArgumentOutOfRangeException&gt;();
        }

        public class Context
        {
            public Context(decimal amounta, decimal amountb)
            {
                AccountA.Balance = amounta;
                AccountB.Balance = amountb;
            }
            public Account AccountA = new Account();
            public Account AccountB = new Account();
        }
    }

 

Edit : After Aaron's remarks I replaced


ItShouldThrowException&lt;ArgumentOutOfRangeException&gt; AmountToHigh;

with


ItShouldFailWithError AmountToLarge = e => e.ShouldBeOfType&lt;ArgumentOutOfRangeException&gt;();

which is closer to the domain expert's language then the developer's.

Now, I do not know what You think, but in my opinion this is a lot more readable then the MSpec version.

While the code currently compiles, the testing itself is not implemented yet, but this should not be such a big hurdle. The automatic catching of the exception should only happen in the "When"-phase afaik, other exceptions should ALWAYS return a fail.

 

Conclusion

In my quest for the holy grail I have met a lot of hurdles but I do think I am getting closer to my target. I hope/think that a domain expert should be able to read the code I present in the specifications; In my opinion this should be a viable DSL-like human-readable text. While the current representation still has some constraints due to it's implementation language, most of the disturbing stuff has been left out.

Up next is the implementation of the testrunner, but I will have to take a look at my schedule when I will be able to complete this, since I sometimes have to do some work that pays the bills as well ;) ...

If you have any suggestions or remarks please do let me know in the comment section !!

Bookmark and Share

About Tom

Tom Janssens op LinkedIn

Tom Janssens op twitter

Core bvba RSS

 

 

Tom Janssens is an independent freelance ICT consultant that has been "into computers" ever since the age of 7.

Typing source code from a book evolved into exploring the limits of coding in procedural, assembly, object-oriented and functional languages.
As he matured in software coding, he started focusing on the problems surrounding software development, and learned that software development is usually about people and interactions first, and about technology second.

Due to his diverse track record he gained insights in a lot of aspects of the software development process. Currently his main focus is on strategic ICT advice, lean product/project development and improving the software development process and architecture.

He avoids ivory-tower-approaches by applying and verifying the applicability of the latest tech buzz in software experiments.

He is also the founder of the following LinkedIn groups:

CQRS Professional
BDD Professional
Asp.Net MVC professional

More info about Tom and his company...

**** Hire me ! ****

My current project will probably run till the end of August 2013. Feel free to contact me for a project later on.

I can work either in Belgium or via remote access!!!

You can find my resume here: resume.pdf .

Community contributions and publications: here .

Call me directly at +32 478 336 376


Advertisement

Forget all your SCRUM -, Kanban - and other Agile and Lean certificates

Here is the only true AGILE and LEAN certificate you will ever need:

The Creative Recursive Analysis Process Certificate
(CRAP Certificate for short)

More info can be found at the official CRAP certificate website:
http://bit.ly/CRAPCertificate