Saturday, August 08, 2009

Misadventures In BDD With NBehave

From my previous post, you might have picked up that I like the idea of BDD's given-when-then construct for describing scenarios.

For a while, I've been wanting to use this syntax in unit/integration tests to make them clearer. But I've been hoping to do that without switching languages or unit-testing frameworks. Toward that end, I tried out NBehave.

Here's some test code that demonstrates my worst fears about why NBehave might not be right for me:

public void Execute()
{
    var story = new Story("Search Contacts by Relationship Strength")
        .AsA("User")
        .IWant("To search on the Strength field")
        .SoThat("I can find Contacts who have a given Strength");

    Scenario scenario = story
        .WithScenario("Find Contacts with Strength of 50");

    scenario.Given("The database contains a Contact with a Strength of 50")
        .And("a search results view", GivenResultsInfo)
        .And("a query for Contacts with Strength of 50",
            GivenQueryForContactsWithStrengthOf50)
        .When("The search is run", WhenRunSearch)
        .Then("The results contain Contacts with Strength of 50",
            ThenShouldContainResultsWithStrengthOf50);
}

private void GivenResultsInfo()
{
    var resultsView = new ReadOnlyResultsView(
        ExtendableEntity.Person, SortType.Ascending, 
        new[] { Column }, Column);
    ResultsInfo = new ResultsInfo(resultsView);
}
private void GivenQueryForContactsWithStrengthOf50()
{
    var condition = 
        new DecimalCondition(Column, ComparisonOperation.EqualTo);
    condition.DataField.Value = 5;

    _query = TypeHelper.GetAdvancedQuery(_entityType);
    _listManager.AddListToAdvancedQuery(_query);
    _query.MainConditionGroup.AddCondition(condition);
}
private void WhenRunSearch()
{
    ISearchService service = TypeHelper.GetSearchService(_entityType);
    ISearchResults results = service.GetResults(_query, ResultsInfo);
    _resultsData = results.Results;
}
private void ThenShouldContainResultsWithStrengthOf50()
{
    Assert.Greater(_resultsData.Rows.Count, 0);

    foreach (DataRow row in _resultsData.Rows)
        Assert.AreEqual(5, (decimal)row[Column.Name]);
}

But for me, creating reports of the text descriptions wasn't the main point. The main point was just to better organize the test. When I started writing the code, I thought maybe I'd just skip the text, but then I thought, "Oh well, I might as well do it."

And that was fine, but when the requirements changed, I had trouble keeping the text synchronized with the code.

After I wrote the initial code, the team decided that instead of having strength on a scale from 0-60, we'd have it on a scale from 0.0-6.0. And when I changed the code, I was in a hurry, since that change hadn't been part of the original estimate, and consequently threatened our timeline.

So I ended up with text that said 50 and code that said 5.

Monday, August 03, 2009

Open Space Coding Day: TDD As If You Meant It and Cucumber With IronRuby

Saturday I was at the Open Space Coding Day that was part of the Alt.Net UK Conference 2009.

In the morning, I was in the session on TDD As If You Meant It. Gojko Adzic already did a pretty good write up, so I'll point you there to learn more about it: TDD as if you meant it – revisited.

In the afternoon, I was in the session about using Cucumber and RSpec with IronRuby. Mostly, I was interested in this because I keep hearing really good things about Cucumber, but as a non-ruby person, I have never quite discovered what those good things actually look like.

I was also a bit psyched, because I was pretty sure I'd heard that IronRuby was going 1.0 on my birthday. But alas, it seems that article subtly changed since I last looked at it, from saying "IronRuby is 1.0!" to now saying "IronRuby is almost at 1.0!" A slight let-down, but that's okay. We still had a good session, and I'm the first person to understand that software doesn't always magically appear on a certain date just because we want it to.

To install IronRuby, Cucumber, and RSpec, I mostly used these instructions:

The process for me went something like this:
  1. Install Ruby.

  2. Install IronRuby. (The day I was doing this, 0.6 was being billed as the current release. It looks like things have moved on.)

  3. From IronRuby and .NET, do:
    1. Installing required gems
    2. Creating a Cucumber wrapper script for IronRuby
    3. Running the examples

  4. Now the examples ran, but there was some weird escape-character output. I didn't find the answer to how to fix this, but another person in the session, Lorenzo Stoakes , did. Here's how that looked on Twitter.

    For convenience, though, I guess I'll compile that into the single sentence: In your file that corresponds to C:/Ruby/lib/ruby/gems/1.8/gems/cucumber-0.3.92/lib/cucumber/formatter/ansicolor.rb, replace Term::AnsiColor.coloring = false if... with Term::AnsiColor.coloring = false.

At that point, the examples seemed to sort-of run okay for me. I think Lorenzo and Garry Shutler got past some warnings that were getting output, but I didn't. I'm also pretty sure Garry also got RSpec to work.

What was the whole point? Well, since I didn't get past the examples, I'll sum it up with one of them.

Say you're developing a calculator. Your business person can specify the addition behaviour in text, like this:

Feature: Addition
  In order to avoid silly mistakes
  As a math idiot 
  I want to be told the sum of two numbers

  Scenario Outline: Add two numbers
    Given I have entered  into the calculator
    And I have entered  into the calculator
    When I press add
    Then the result should be  on the screen

  Examples:
    | input_1 | input_2 | output |
    | 20      | 30      | 50     |
    | 2       | 5       | 7      |
    | 0       | 40      | 40     |

And you can make that executable by adding some code in a related code file like this, where you're testing a .NET assembly Calculator.dll that contains a Calculator class in the namespace Demo, and the Calculator class has push and Add methods:

require 'spec/expectations'
$:.unshift(File.dirname(__FILE__) + '/../../lib') # This line is not needed in your own project
require 'Calculator' # Calculator.dll

Before do
  @calc = Demo::Calculator.new # A .NET class in Calculator.dll
end

Given "I have entered $n into the calculator" do |n|
  @calc.push n.to_i
end

When /I press add/ do
  @result = @calc.Add
end

Then /the result should be (.*) on the screen/ do |result|
  @result.should == result.to_i
end

That does look pretty sweet.