implements Elegance {

// Elwyn Malethan's musings on software development, mountain biking and general navel–gazing...

Articles tagged with 'bdd'

JavaScript BDD framework in less than 200 lines

I‘ve been doing a lot of work with JavaScript lately and so I‘ve been thinking a lot about how to effectively bring my experiences of BDD and TDD in other technologies to the party. There are a few test frameworks available, JsTestDriver by far the better looking of the lot. There is a specification framework, based on rspec as well. However, I couldn't find a JavaScript BDD story framework anywhere. So armed with some new knowledge from JavaScript: The Good Parts, I had a go at looking into what such a framework would look like. I was surprised as to how quickly I came up with a solution and how small the footprint of the end result is.

So here it is, in less than 200 lines, a JavaScript BDD Framework I've called it BehaviourJS and I've made it available under the GNU Lesser General Public License.

// ------------------------------------------------------------ Function augmentation

/**
 * Standard means of augmenting everything with new methods
 *
 * @param name the name of the new method
 * @param func the function we want invoked
 */
Object.prototype.method = function(name, func) {
    if (!this.prototype[name]) {
        this.prototype[name] = func;
        return this;
    }
};

// ------------------------------------------------------------ Should DSL

behaviourJs = {};

behaviourJs._shouldBe = function(expected) {
    if(expected !== null) {
        var exp = expected.valueOf();
        var self = this.valueOf();
        if(self === exp) {
            return;
        }
    }
    throw {
        name: 'AssertionError',
        message: 'Expected ' + expected + ' but was ' + this
    };
};

Object.method('shouldBe', behaviourJs._shouldBe);
Object.method('shouldEqual', behaviourJs._shouldBe);
Object.method('shouldBeEqual', behaviourJs._shouldBe);
Object.method('shouldBeEqualTo', behaviourJs._shouldBe);

behaviourJs._shouldNotBe = function(expected) {
    if(expected !== null) {
        var exp = expected.valueOf();
        var self = this.valueOf();
        if (self !== exp) {
            return;
        }
        throw {
            name: 'AssertionError',
            message: 'Expected not to be ' + expected + ' but was ' + this
        };
    }
};

Object.method('shouldNotBe', behaviourJs._shouldNotBe);
Object.method('shouldNotEqual', behaviourJs._shouldNotBe);
Object.method('shouldntBe', behaviourJs._shouldNotBe);
Object.method('shouldntEqual', behaviourJs._shouldNotBe);

// ------------------------------------------------------------ StoryInstance

behaviourJs.StoryInstance = function(block) {
    block.call(this);
};

behaviourJs.StoryInstance.prototype.visit = function(visitor) {
    visitor.visitStory(this);
};

behaviourJs.StoryInstance.prototype.run = function() {
    this.visit(new behaviourJs.StoryRunner());
};

// ------------------------------------------------------------ ScenarioInstance

/**
 * A runnable scenario.
 *
 * @param name the description of the scenario
 * @param block the function containing the contents of the scenario (usually
 * a series of Givens,When and Thens).
 */
behaviourJs.ScenarioInstance = function(name, block) {
    this.name = name;
    this._steps = [];

    behaviourJs.ScenarioInstance.__currentScenario = this;

    block.call();

    behaviourJs.ScenarioInstance.__currentScenario = null;
    behaviourJs.ScenarioInstance.__currentPhase = null;
};

behaviourJs.ScenarioInstance.__currentScenario = null;

behaviourJs.ScenarioInstance.__currentPhase = null;

behaviourJs.ScenarioInstance.prototype.addStep = function(step) {
    this._steps[this._steps.length] = step;
};

behaviourJs.ScenarioInstance.prototype.visit = function(visitor) {
    visitor.visitScenario(this);
};

// ------------------------------------------------------------ Step

behaviourJs.Step = function(type, meta, block) {
    this._type = type;
    this._meta = meta;
    this._block = block;
};

behaviourJs.Step.prototype.visit = function(visitor) {
    visitor.visitStep(this);
};

// ------------------------------------------------------------ StoryRunner

behaviourJs.StoryRunner = function() {
    this.storyCtx = {};
};

behaviourJs.StoryRunner.prototype.visitStory = function(story) {
    var title = 'Story: ' + story.description;
    document.writeln(title);
    var line = '';
    for(var i = 0; i < title.length; ++i) {
        line += '=';
    }
    document.writeln(line);
    for (var scenIndex = 0; scenIndex < story.scenarios.length; ++scenIndex) {
        story.scenarios[scenIndex].visit(this);
    }
};

behaviourJs.StoryRunner.prototype.visitScenario = function(scenario) {
    document.writeln('Scenario: ' + scenario.name);
    for (var stepIndex = 0; stepIndex < scenario._steps.length; ++stepIndex) {
        var step = scenario._steps[stepIndex];
        step.visit(this);
    }
    document.write("\n");
};

behaviourJs.StoryRunner.prototype.visitStep = function(step) {
    var stepDesc = step._type.toUpperCase() + ' ' + step._meta;
    if (step._block === undefined || step._block === null) {
        if (step._type === 'then') {
            stepDesc += ' [PENDING]';
        }
    } else {
        try {
            step._block.call(this.storyCtx);
        } catch(e) {
            if (e.name == 'AssertionError') {
                stepDesc += ' [FAIL : ' + e.message + ']';
            } else {
                stepDesc += ' [ERROR (' + e.name + ') : ' + e.message + ']';
            }
        }
    }
    document.writeln(stepDesc);
};

// ------------------------------------------------------------ DSL

Story = function(block) {
    return new behaviourJs.StoryInstance(block);
};

Scenario = function(name, block) {
    return new behaviourJs.ScenarioInstance(name, block);
};

Given = function(desc, block) {
    var scenario = behaviourJs.ScenarioInstance.__currentScenario;
    behaviourJs.ScenarioInstance.__currentPhase = 'given';

    scenario.addStep(new behaviourJs.Step(behaviourJs.ScenarioInstance.__currentPhase, desc, block));
};

When = function(desc, block) {
    var scenario = behaviourJs.ScenarioInstance.__currentScenario;
    behaviourJs.ScenarioInstance.__currentPhase = 'when';

    scenario.addStep(new behaviourJs.Step(behaviourJs.ScenarioInstance.__currentPhase, desc, block));
};

Then = function(desc, block) {
    var scenario = behaviourJs.ScenarioInstance.__currentScenario;
    behaviourJs.ScenarioInstance.__currentPhase = 'then';

    scenario.addStep(new behaviourJs.Step(behaviourJs.ScenarioInstance.__currentPhase, desc, block));
};

And = function(desc, block) {
    var scenario = behaviourJs.ScenarioInstance.__currentScenario;
    scenario.addStep(new behaviourJs.Step(behaviourJs.ScenarioInstance.__currentPhase, desc, block));
};

My approach is heavily influenced by my experiences of using Easyb for BDD for Java. There are a few things I haven‘t added, such as before and after hooks.

Stacktrace support in JavaScript is sketchy and differs greatly across runtimes/browsers. One solution might be to integrate this attempt at universally obtaining a stacktrace. The should DSL is not as comprehensive as Easyb. Unlike Easyb the should DSL in BehaviourJS is completely extensible. This is possible because of the nature of the JavaScript langauge.

I mentioned runtimes/browsers earlier. The other potential issue is that I have only tested this in Firefox. It‘s the product of a couple of hours hacking so it‘ll be no surprise if it fails in other browsers.

Runners, build system and IDE support.

I realise that just implementing such a framework isn‘t even half of the effort required for a test framework to enable developers to practice TDD or BDD. For this framework to be useful to me or anyone else, it will need IDE support (IDEA and Eclipse at least) and build system support (Maven and maybe Ant).

The existing JavaScript test frameworks have had a lot of work put into this area and it shows. Especially with JsTestDriver‘s IDE support, which is awesome.

Maybe next weekend I‘ll have a look at plugin this into an existing test framework, such as JsTestDriver.

Example story

A story using the above framework might look something like this.

Story(function() {
    this.description = "This is an example story";
    this.summary = {
        as_a : "role",
        i_want : "to perform some action",
        so_that : "there is some perceived benefit"
    };

    this.scenarios = [
        Scenario("Some scenario", function() {
            Given("some string", function() {
                this.someString = 'this';
            });
            And("some number", function() {
                this.ten = 10;
            });
            When("something happens");
            Then("some condition is evaluated", function() {
                this.someString.shouldNotBe(null);
                this.someString.shouldNotBe({});
                this.someString.shouldNotBe(123);
                this.someString.shouldNotBe('that');

                this.someString.shouldBe(this.someString);
                this.someString.shouldBe('this');
            });
            And("some other condition is evaluated", function () {
                this.ten.shouldBe(10);
            });
        }),
        Scenario("Checking incorect values", function() {
            Given("some new precondition");
            And("some other new precondition");
            When("something new happens", function() {
                this.animal = 'animal';
            });
            Then("some new condition is evaluated", function() {
                try {
                    thisVar.doesNotExist();
                } catch(e) {
                    // gulp
                }
            });
            And("some other new condition is evaluated", function() {
                try {
                    this.animal.shouldBe('farm');
                } catch(e) {
                    e.name.shouldBe('AssertionError');
                }
            });
        }),
        Scenario('some complex objects', function() {
            Given(' a complex object', function() {
                this.MyThing = function(val) {
                    this._val = val;
                };

                this.someObj = new this.MyThing('a value');
            });
            And('a copy of it', function() {
                this.sameObj = this.someObj;
            });
            And('a different object', function() {
                this.differentObj = new this.MyThing('a value');
            });
            Then('the object and the copy should be the equal', function() {
                this.someObj.shouldBe(this.sameObj);
                this.sameObj.shouldBe(this.someObj);
            });
            And('the object and the different one should not', function() {
                this.someObj.shouldNotBe(this.differentObj);
                this.differentObj.shouldNotBe(this.someObj);
            });
        })
    ];
}).run();

The link to JavaScript: The Good Parts above is an affiliate link. It's a great book, written by a programmer for programmers.

First published on Jan 24, 2010. Last updated on: Jan 25, 2010.

Taking the fear out of change: BDD is essential

During the lifetime of a project, requirements are going to change. The world moves quickly, customers change their minds. You‘re just going to have to deal with it .

So, do you choose a development methodology that tries to manage or contain that change or do you accept the fact that it‘s going to happen and embrace that change?

There are two things I always keep in mind when working on a software project. These are Kent Beck's you ain't gonna need it and Rory Marinich's be less shit.

Keep the requirements brief and focussed: only enough for 2–3 weeks of work. Few people have the attention span for more. Deliver only what you know you need and then revise according to the inevitable evolution in priorities — eradicate any shortcomings, de–shit. The only software development model that supports this is one that repeatedly releases software in short iterations.

It also has to be one that takes the fear out of change. A really good way to do that is to have the security blanket of high unit–test coverage. A good way to get high unit–test coverage is to TATFT. An automatic way to TATFT is let the requirements drive and shape your code by using BDD.

So, embrace change, test–first, use BDD and TATFT.

First published on Aug 17, 2009. Last updated on: Dec 29, 2009.

Self documenting software

Documenting requirements then implementing those requirements and then documenting that implementation is bad enough. Maintaining that documentation when requirements and implementation changes definitely makes documentation, generally a problem looking for a better solution. BDD to the rescue.

Not for a second am I suggesting that the necessary documentation shouldn‘t be produced or that it does not provide essential value to a project. What I am saying is that it should be easier and less laborious to produce. In an Agile environment it should also stay as close to the code as possible. Otherwise, it is likely to be out of date as soon as the first line of code is written.

Stories

When I was at Beanlogic, we covered most of our Ruby/Rails work with Rspec specs and stories. One handy outcome of running Rspec specs and stories is that a nicely formatted HTML document is generated that is readable to civilians. In fact in the case of stories, human–friendly prose actually translate into code coverage. Something we planned to do at the time was to make more use of this feature.

In order to tell good stories and for them to be most valuable to a project, the customer/owner of any project should be encouraged to produce structured stories with scenarios in a Given–When–Then format. This would of course be done in a supportive, collaborative way — perhaps via a wiki — since not all customers have the experience or skill–sets to work in such a prescribed way.

For example, a scenario that a customer operating a luxury holiday cottages website might – with the right guidance – produce the following scenario.

Given a user is searching for cottages
And they provide the search term 'fireplace'
When they perform the search
Then the results should contain all cottages with a fireplace

Obviously, this scenario would be split into finer–grained scenarios describing the lower–level implementation.

Using BDD (and Rspec in particular) in this way means the documentation is a by-product of the development process and is intrinsically linked to the code. This means that if requirement–creep leads to implementation changes, the documentation stays current.

Not only that, what we‘ve just done is managed to get our customer to write our top–level tests for us! Surely this is FTW!

I‘m never sure of the right context in which to use FTW or FAIL or other such intarwebz expressions. On the other hand, I‘m pretty sure I overuse WTF!

easyb

Given that I am ‘into‘ TDD, BDD, Agile and all that best–practice stuff (not everyone is); and given that I even navel–gazed about a possible Java implementation of Rspec's specs, I was shocked to find that easyb had completely flown under my radar until I stumbled upon it last week. Well, I feel like I‘ve only scratched the surface, but my initial impression is good.

I‘ll be furiously trying easyb out on testing this website and SeemoreJ in the next few days and I‘ll be posting my experience here.

First published on May 20, 2009. Last updated on: Dec 30, 2009.

 
People I like
Other sites