Tuesday, August 7, 2007

Addressing some Behave# concerns

So Joe and I have received some initial feedback for Behave#.  Joe's already given a great intro into how to use Behave# and addressed Roy's specific questions.  I thought I'd address some of the common issues regarding Behave#:

  • Using string matching
  • Using anonymous delegates
  • One developer supports it

Using string matching

One common concern I've heard from a couple of sources now is that Behave# uses strings to match behavior for subsequent scenarios.  Something that might not be clear on how we match behavior is that Scenarios are scoped to a Story.

That is, the "context" parameter of a "Given" scenario fragment is only able to be matched against other Scenarios within a single Story.  Here's an example:

public void Withdraw_from_savings_account()

    Account savings = null;
    Account cash = null;

    Story transferStory = new Story("Transfer to cash account");

        .AsA("savings account holder")
        .IWant("to transfer money from my savings account")
        .SoThat("I can get cash easily from an ATM");

        .WithScenario("Savings account is in credit")
        .Given("my savings account balance is", 100,
               delegate(int accountBalance) { savings = new Account(accountBalance); })
        .And("my cash account balance is", 10,
             delegate(int accountBalance) { cash = new Account(accountBalance); })
        .When("I transfer to cash account", 20,
              delegate(int transferAmount) { savings.TransferTo(cash, transferAmount); })
        .Then("my savings account balance should be", 80,
              delegate(int expectedBalance) { Assert.AreEqual(expectedBalance, savings.Balance); })
        .And("my cash account balance should be", 30,
             delegate(int expectedBalance) { Assert.AreEqual(expectedBalance, cash.Balance); })

        .Given("my savings account balance is", 400)
        .And("my cash account balance is", 100)
        .When("I transfer to cash account", 100)
        .Then("my savings account balance should be", 300)
        .And("my cash account balance should be", 200);

    Story withdrawStory = new Story("Withdraw from savings account");

        .AsA("savings account holder")
        .IWant("to withdraw money from my savings account")
        .SoThat("I can pay my bills");

        .WithScenario("Savings account is in credit")
        .Given("my savings account balance is", 100); // This entry doesn't have a match!


In the "withdrawStory", even though the "Given" fragment string matches the "transferStory" "Given" fragments, the behavior will not match up.  That's because Scenarios belong to a Story, and the matching only happens within a given Story.

In DDD terms, the Aggregate Root is the Story, and the child Entities include the Scenarios.  We could match across stories, but that wouldn't adhere to DDD guidelines, and would result in much more complexity.

So matching issues only happen within one Story.  I don't know how many Scenarios you would need to write before running into issues, but I think we could follow some of Joe's suggestions and use some more intelligent matching algorithms.

Using anonymous delegates

Let me be the first to admit that anonymous delegates are clunky, difficult, and just plain ugly.  But keep in mind that Behave# only deals with delegates.  How the consuming test code creates these delegates does not matter to Behave#.  We have several options (asterisk next to C# 3.0 features):

Anonymous methods may not be very prevalent in C# 2.0, but there are still quite a few classes in the DNF that use delegate arguments for method parameters.  I ran the following code against .NET 3.5 assemblies:

var types = from name in assemblyNames
            select Assembly.LoadWithPartialName(name) into a
            from c in a.GetTypes()
            where (c.IsClass || c.IsInterface) && c.IsPublic && !c.IsSubclassOf(typeof(Delegate))
            select c.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) into methods
            from method in methods
            where method.GetParameters().Any(pi => pi.ParameterType.IsSubclassOf(typeof(Delegate)))
                && !method.Name.StartsWith("add_", StringComparison.OrdinalIgnoreCase)
                && !method.Name.StartsWith("remove_", StringComparison.OrdinalIgnoreCase)
            select new { TypeName = method.DeclaringType.FullName, MethodName = method.Name };

int methodCount = types.Count();
int typeCount = types.GroupBy(t => t.TypeName).Count();

Debug.WriteLine("Method count: " + methodCount.ToString());
Debug.WriteLine("Type count: " + typeCount.ToString());

And I found that there are 1019 methods with delegate parameters spread out over 155 types.  With the System.Linq.Enumerable extension methods in .NET 3.5, methods with delegate parameters will be used much more often.

Only one developer supports it

Well...not exactly true.  There are two developers, Joe and I, so that's a 100% improvement, right? :)

Wrapping it up

I really like Dan North's rbehave.  Behave# closely matches rbehave's usage and intent.  Are a lot of the same issues regarding Behave# also valid for rbehave?  Ruby lends itself well to BDD, especially when combining rbehave and rspec, with the elegance of dynamic typing and language features like blocks.

Behave# is still getting started, and we have some kinks to iron out, but I do think we're on the right track, following in Dan North's footsteps.

1 comment:

TexicanJoe said...

Excellent point about one developer supporting it. I don't know how I overlooked that. It could be because I was writing it at 2AM but couldn't agree with you more. Thanks for clarifying the delegate support.