Howto+why using a prototype & bdd for a project estimate + personal rant

by Tom 22. December 2009 13:57

Introduction

As you might or might not now, estimating the budget for a project can be very hard sometimes. In order to get started on a project we first need a real estimate of what the client will need, and the client should know what I am going to deliver to him/her.
What I personally learned from the past is that one can never be fine-grained enough, since there will always be mismatches somewhere.

We can however try to offer the client something that he/she can both understand and work with, next to a description of what everything should do (from a business point of view).

How can we do this, while investing as little work as possible ?

 

Unfortunately one can not correctly estimate projects without doing some work

As straightforward as this might seem, most people do not seem to be able to grasp this ( do not be ashamed; I have been guilty as well ).

In order to give a correct estimate, you have to INVEST a decent amount of TIME.

 

How can we make sure that we have as little mismatches as possible ?

You can do this by making sure the estimate is complete; a short list of possible deliverables:

  • User manual
  • Technical reference manual
  • Quickstart guide
  • Sourcecode
  • Educating the endusers
  • A scope definition for the software
  • A feature definition within the software scope

Building this list is usually really easy, except for the last 2 parts:


Scope and feature definition for the software

This commonly known cartoon is unfortunately true in most cases (click to go the original one) :

Project planning cartoon

Defining the scope of the software is usually hard work, and most of the time the expectations of the client do not match the planned implementations of the developer. How can we avoid this pitfall ???

This is quite easy, but it does require some work: you build a prototype/wireframe of the application, and show this to the client, asking if everything he wants is in there. This requires quite some time, but usually your quote will be selected because they can see how much time you invested in it, and it shows just how professional you approach your work.

Next to this you should start on some high level BDD stories describing the behaviour of the problem domain and the application. This means writing stories from a domain perspective (i.e. no computers involved) as well as from an app perspective if necessary.

 

But how do I do this technically ?

Well, that is completely up to yourself. In order to get you started I can show you how I do it :

For the prototyping I currently use a combination of HTML/jQuery/jQuery UI and a jQuery plugin called polypage.

You can see a screenshot here:

I am using jQuery UI for tabs and an accordion to define different possible steps of a screen. Next to this I use polypage to switch different states (you can click on the "logged in" link on the left top in orde to switch between a user that is logged in, and one that is not logged in.)

You can look at the sample here : http://www.corebvba.be/demos/prototype/index.html . Please note that this is pure HTML/Javascript, so there is no serverside involved here... So if you save the html/css and script sources you have everything I have... It might also be interesting to know that there is only a single HTML file: index.html. All the necessary code is in there.

Next to this I describe my domain features using BDD/the ubiquitous language. This will later be fed back into the program tests using BDD. For a good introduction on BDD I should redirect you to Dan North.

Describing the domain functionality is needed in order to make sure that you can also estimate your resource usage for everything that happens behind the scenes (which will probably be the biggest part in enterprise projects).

It goes without saying that I do use my very own BDD framework as a BDD implementation framework later on.

 

In conclusion

If you use both HTML wireframes/prototypes together with BDD to describe the problem domain, you usually can grasp wath the project is about, and you should be able to give some good estimates on the budget for a project. Another advantage of this technique is that your quote does show how much time you already invested in their project, and how professional you are about it. This will probabaly give you a commercial advantage over other quotes, since most of them will probably not be as detailed as this one.

I do bill some of my work from this phase back to the client if I do get the project, but usually this is not a problem, since the client can see the value of doing this.

 

A warning/personal rant

I also realize that this approach is not always possible, since some clients are not willing to invest enough time in order to give you enough information. Based on my previous experience with this kind of situation I can only advise you one thing: turn around and run away from it as fast as you can, especially in combination with fixed-price projects !! These kind of clients will never know what the final scope and features of the application will be, so you will have to do dozens of rewrites because they do not know the domain or scope themselves, and implement all kinds of extra functionalities that were never mentioned in the initial scope!!! It implies waterfall development, but in the worst possible way !!!

A few typical taglines by which you can smell problems before beginning on the project:

  • "Oh that is something we do not know yet, but it shouldn't be that hard."
  • Stakeholders having discussions over domain problems
  • "It should be something like site XXX". FINAL STOP. (no further details provided)
  • "We need to interface some external apps... we'll see which ones later on"
  • "How much data.. oh we do not know yet"

A few typical lines by which you can notice you are in trouble while working on a project:

  • "When I say I want to upload files in internet explorer, I do mean any file, even it is larger then 200MB"
  • "What ? Your CSS layout does not support Internet explorer 4 ? Fix it asap"
  • "Uploading the file repository is slow; I don't care that I upload 2GB of data over a DSL line; it is slow, you should fix it"
  • "Oh, but we do need both implicit and explicit fine-grained security, in such a way that we do not know yet ourselves. Just implement something, and we'll let you know if it is ok"
  • "Yes can you do this ? How much time does it take ? 10 days ?" => 20 days later: "Ok, we want it by next week..."
  • "Oh, we decided to use the app for all our customers instead of just ourselves... Why doesn't your app support the load of umphteen times the original amount of users/data ?"
  • "BTW I forgot to tell you, we need custom theming for every customer group, and we should be able to do it ourselves... No, we do not want to edit aspx/css files, just make an editor, ok ?"
  • And finally : "What do you mean, another invoice ? We are not paying since your original quote stated xxx" after investing y times the work estimated in the original quote... "Yeah, we'll see you in court, which means you can wait another year for your money (if you are lucky)"

As hilarious as this list may seem, these are all things that I have experienced in real life... So please take my advice and run away as fast as you can from such clients...

PS: for really professional people not willing to invest too much time in a project quote, I can always advise this site.

Bookmark and Share

Tags: , , , , ,

CodeProject | Development | Howto | News | Tom's blog

Aubergine (BDD for .Net) source code available on github

by Tom 20. November 2009 11:01

Hi there....

After a few weeks of working on Aubergine (BDD for .Net) I have decided to release the sourcecode for this tool out in the open.

Please do give me some time as I am quite new to this whole community thing. Hints are welcome !

You can find the source on github : http://github.com/ToJans/Aubergine

And the download here: http://github.com/ToJans/Aubergine/downloads

Enjoy !!!

 

 

Bookmark and Share

Tags: , , , , ,

CodeProject | Development | News | Tom's blog

Aubergine (BDD for .net) v0.07 : support for "Given I did"-clause

by Tom 13. November 2009 17:12

Hi there,

V0.07 is here :

Auberginev0.07.zip (28,61 kb)

Changes:

* Support for "Given I did" clause (more info in this essay )
* Complete refactoring of the Account example after extending the account story (my usual Model/Service layout, although the interfaces are missing here)

Here is the current example of the account story (copied from the essay):

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;
        }
    }

 

Bookmark and Share

Tags: , , ,

New essay on BDD - "Given I did" to support flow in user stories

by Tom 13. November 2009 06:04

Just a quick link to my new essay on BDD:

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

I hope this is a good contribution to the BDD community

Bookmark and Share

Tags: ,

Development | News | Tom's blog

Aubergine (BDD for .net) v0.06 : support for parameter tables in given/when/then

by Tom 12. November 2009 05:30

Ok, we keep on going; now we support parameter tables for given/when/then, which are parsed as array members.

I am now getting very close to full cucumber-like support !!! Oh, I forgot to mention that the "And" keyword is also supported now !!

An example of the new syntax:


Context    Be.Corebvba.Aubergine.Examples.Website.Contexts.BrowserContext
ContextDLL Be.Corebvba.Aubergine.Examples.DLL

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

    Scenario Search results for 'keywords' on searchengine should contain 'www.corebvba.be'
        Given the current url is 'search url'
        When searching for 'keywords'
        Then the result should contain 'www.corebvba.be'
        Example
        +--------------+--------------------------------+------------------+
        | searchengine | search url                     | keywords         |
        +--------------+--------------------------------+------------------+
        | google       | http://www.google.be/search?q= | BDD .Net         |
        | bing         | http://www.bing.com/search?q=  | core bvba tom    |
        | bing         | http://www.bing.com/search?q=  | Quantum physics  |
        | faulty link  | http://www.googleaaa           | core bvba tom    |
        +--------------+--------------------------------+------------------+


    Scenario Search results on google for keywords should contain 'www.corebvba.be'
        Given the current url is 'http://www.google.be/search?q='
        When searching for the following keywords
            +-----------+
            | keywords  |
            +-----------+
            | Aubergine |
            | BDD       |
            +-----------+
        Then the result should contain 'www.corebvba.be' and the following markup elements
            +------------------+
            | type | inner     |
            +------------------+
            | em   | BDD       |
            | em   | Aubergine |
            +------------------+

And the context :


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

        private WebClient wc = new WebClient();
           
        [DSL("the current url is '(?&lt;url&gt;.*)'")]
        void SetUrl(string url)
        {
            Url = url;
        }

        [DSL("searching for '(?&lt;keywords&gt;.*)'")]
        void SearchForKeyWords(string keywords)
        {
            Result = wc.DownloadString(Url + HttpUtility.UrlEncode(keywords));
        }

        [DSL("searching for the following keywords")]
        void SearchForKeyWords(string[] keywords)
        {
            Result = wc.DownloadString(Url + HttpUtility.UrlEncode(string.Join(" ",keywords)));
        }


        [DSL("the result should contain '(?&lt;myurl&gt;.*)'")]
        bool ResultShouldContain(string myurl)
        {
            return (Result??"").Contains(myurl);
        }

        [DSL("the result should contain '(?&lt;avalue&gt;.+)' and the following markup elements")]
        bool ResultShouldContain(string avalue,string[] type,string[]inner)
        {
            if (string.IsNullOrEmpty(Result) ||type.Length==0)
                return false;
            if (!Result.Contains(avalue)) return false;
            for (var i = 0;i &lt; type.Length ; i++)
            {
                var searchstring = string.Format("&lt;{0}&gt;{1}&lt;/{0}&gt;", type[i], inner[i]);
                if (!Result.Contains(searchstring))
                    return false;
            }
            return true;
        }
    }

It looks a bit messy, but thats because it is a stupid example. A good example would be loading a repository with some products.

Download it here !!

Auberginev0.06.zip (41,70 kb)

Enjoy !!

Bookmark and Share

Tags: , , , ,

CodeProject | Development | News | Tom's blog

Aubergine (BDD for .net) v0.05 : text stories/html & custom output/cmdline parser

by Tom 11. November 2009 21:29

Again a new release !!

* The stories are now written in text-format; unrecognized beginning of lines are skipped/not processes; example data should begin with/be seperated by "|".
* Commandline parser implemented with help

Edit 3 : v0.06 arrived

More info here !!

 

C:\Projecten\Be.Corebvba.Aubergine\Be.Corebvba.Aubergine.Examples\Lib&gt;ConsoleRunner.exe /h
Aubergine Console Runner - Core bvba - Tom Janssens 2009

Usage :

ConsoleRunner [wildcard or filename for story file][...] [parameters]

Where parameters can be one of the following :

-html
Output to html unordered lists

-fmthdr,-formatheader XXXX
Header to put before starting the children output; example "&lt;ul&gt;"

-fmt,-formatter XXXX
Named storyformatter; example "&lt;li&gt;{Type} {Description} &lt;a href='#' title='{StatusInfo}'&gt;{StatusText}&lt;/a
&gt;&lt;/li&gt;"

-fmtftr,-formatfooter XXXX
Footer to put after ending the children output; example "&lt;/ul&gt;"

-h,-?,-help
Show the syntax of the commandline parameters

An example story :


Context    Be.Corebvba.Aubergine.Examples.Website.Contexts.BrowserContext
ContextDLL Be.Corebvba.Aubergine.Examples.DLL

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

    Scenario Search results for keywords on searchengine should contain my url
        Given the current url is search url
        When searching for keywords
        Then the result should contain my url
  
        +--------------+--------------------------------+------------------+-----------------+
        | searchengine | search url                     | keywords         | my url          |
        +--------------+--------------------------------+------------------+-----------------+
        | google       | http://www.google.be/search?q= | BDD .Net         | www.corebvba.be |
        | bing         | http://www.bing.com/search?q=  | core bvba tom    | www.corebvba.be |
        | bing         | http://www.bing.com/search?q=  | Quantum physics  | www.corebvba.be |
        | faulty link  | http://www.googleaaa           | core bvba tom    | www.corebvba.be |
        +--------------+--------------------------------+------------------+-----------------+

The code is still the same :


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Web;
using Be.Corebvba.Aubergine.Model;

namespace Be.Corebvba.Aubergine.Examples.Website.Contexts
{
    public class BrowserContext
    {
        public string Url { get; set; }
        public string Result { get; set; }

        private WebClient wc = new WebClient();
           
        [DSL("the current url is (?&lt;url&gt;.*)")]
        void SetUrl(string url)
        {
            Url = url;
        }

        [DSL("searching for (?&lt;keywords&gt;.*)")]
        void SearchForKeyWords(string keywords)
        {
            Result = wc.DownloadString(Url + HttpUtility.UrlEncode(keywords));
        }

        [DSL("the result should contain (?&lt;myurl&gt;.*)")]
        bool ResultShouldContain(string myurl)
        {
            return (Result??"").Contains(myurl);
        }
    }
}

And this is the resulting output (please try to hover over the implementation error of the "When" step with your mouse !!

Edit : apparently my blog engine team messes up the list layout !!! It are nested list items (although you can not see it here).

Processing file(s) : Website\Stories\*.txt

  •  
    •  
      • Story Make sure my website gets enough visibility IMPLEMENTATION ERROR
        • Scenario Search results for BDD .Net on google should contain www.corebvba.be OK
          • Given the current url is http://www.google.be/search?q= OK
          • When searching for BDD .Net OK
          • Then the result should contain www.corebvba.be OK
        • Scenario Search results for core bvba tom on bing should contain www.corebvba.be OK
          • Given the current url is http://www.bing.com/search?q= OK
          • When searching for core bvba tom OK
          • Then the result should contain www.corebvba.be OK
        • Scenario Search results for Quantum physics on bing should contain www.corebvba.be NOK
          • Given the current url is http://www.bing.com/search?q= OK
          • When searching for Quantum physics OK
          • Then the result should contain www.corebvba.be NOK
        • Scenario Search results for core bvba tom on faulty link should contain www.corebvba.be IMPLEMENTATION ERROR
          • Given the current url is http://www.googleaaa OK
          • When searching for core bvba tom IMPLEMENTATION ERROR
          • Then the result should contain www.corebvba.be NOK
  • Processing file : C:\Projecten\Be.Corebvba.Aubergine\Be.Corebvba.Aubergine.Examples\bin\Release\Website\Stories\Make_sure_my_Website_gets_enough_traffic.txt

This is the postbuildstep now :


"$(ProjectDir)\lib\ConsoleRunner.exe" Accounts\Stories\*.txt Website\Stories\*.txt -html > "$(TargetDir)output.html"
"$(TargetDir)output.html"
exit 0

Include all the stories/txt-file in your project with the option of "copy to output directory" = "always" on each file...

More details/a codeproject article update will follow later...

Auberginev0.05.zip (24,86 kb)

Edit 2 : Apparently the reference is wrong (rereference Be.corebvba.aubergine.dll under the lib folder)

 

Enjoy !!

Bookmark and Share

Tags: , , , , ,

CodeProject | Development | News | Tom's blog

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

Tags: , , , , , , , ,

CodeProject | Development | Howto | Tom's blog

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(@"(?&lt;account&gt;Account[AB])_has_(?&lt;amount&gt;\d+)_m")]
        void accountX_has_Ym(Account account, decimal amount)
        {
            account.Balance = amount * 1m;
        }

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

        #endregion


        #region When

        [DSL(@"transfering_(?&lt;amount&gt;\d+)_m_from_(?&lt;from&gt;Account[AB])_to_(?&lt;to&gt;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_(?&lt;amount&gt;\d+)_m_on_(?&lt;account&gt;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("(?&lt;name&gt;Account[AB])")]
        Account getaccountAB(string name)
        {
            return this.Get&lt;Account&gt;(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

Tags: , , , , ,

CodeProject | Development | News | Tom's blog

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

Tags: , , , , , , , ,

CodeProject | Development | News | Tom's blog

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

Tags: , , , , , , ,

CodeProject | Development | News | Tom's blog

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About me

Hello,

My name is Tom, I am a father of 2 sons named Quinten and Matisse, and married to Liesbeth.
ICT is both my job and passion.
Next to this my other hobby is actively playing music (mostly guitar).

Things I do to make a living (and I am for rent) :

  • Designing/extending .Net application architectures (mostly asp.net mvc)
  • Code review
  • Coaching
  • Teaching
  • Project management
  • ... basicly almost anything ICT related

You can find more info about my professional carreer on my LinkedIn profile

Kind regards,

Tom