A new BDD framework in .Net : Aubergine

REMARK : An executable testrunner and some sample code is available

Here : /posts/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<Context>
        {
            Given AccountAHas1M;
            Given AccountBHas1M;
            When Transfering1MFromAccountAToAccountB;
            Then ShouldHave0MOnAccountA;
            Then ShouldHave2MOnAccountB;
        }

        class TransferTooMuch : Scenario<Context>
        {
            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<Context>
        {

            public bool Get(string name,Context ctx)
            {
                return
                    Implement(@"(Account[AB])Has(\d+)M", (c, e) =>
                        {
                            e.ByName<Account>(0,c).Balance = e.Get<int>(1) * 1m;
                        }).Run(name, ctx) &&
                    Implement(@"ShouldHave(\d+)MOn(Account[AB])", (c, e) =>
                        {
                            e.ByName<Account>(1,c).Balance.ShouldEqual(e.Get<int>(0) * 1m);
                        }).Run(name, ctx) &&
                    Implement(@"Transfering(\d+)MFrom(Account[AB])To(Account[AB])", (c, e) =>
                        {
                            e.ByName<Account>(1,c).Transfer(e.Get<decimal>(0) * 1m, e.ByName<Account>(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<Context> AccountA_has_1m;
        Given<Context> AccountB_has_1m;
       
        class Transfer_1m_between_2_accounts : Scenario<Context>
        {
            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<Context>
        {
            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<Context>
        {
            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<Context> Dsl_for_Context = new Dsl_for<Context>();

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

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

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

                Dsl_for_Context[@"the_current_user_is_authenticated_for_(Account[AB])"] = (c, e) =>
                   c.Get<Account>(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<Account>(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<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(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<Account>(x).Balance = y.As<decimal>()*1m;
        }

        [DSL(@"should_have_(\d+)_m_on_(Account[AB])")]
        void should_have_Xm_on_AccountY(string x, string y)
        {
            this.Get<Account>(y).Balance.ShouldEqual(x.As<decimal>() * 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<Account>(a).Transfer(x.As<decimal>() * 1m, this.Get<Account>(b));
        }

        [DSL(@"the_current_user_is_authenticated_for_(Account[AB])")]
        void authenticate_for_account_x(string x)
        {
          this.Get<Account>(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<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","https://www.google.be/search?q=","core bvba tom janssens","www.corebvba.be")]
    [Data("google","https://www.google.be/search?q=","BDD .Net","www.corebvba.be")]
    [Data("bing","https://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 "https://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 "https://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 "https://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