implements Elegance {

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

Articles tagged with 'javascript'

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.

TDD in JavaScript intensive software

I recently switched jobs. I left Smartstream for somewhere closer to home. I did this primarily to avoid the commute from Cardiff to Aztec West, in Bristol. I'll be glad to avoid that drive on the M4 every day. I joined Move, who are based in Utah but have a development office not too far from Cardiff.

Having been working in the familiar world of server–side Java before Christmas, I‘ve now moved to the team that work on what we call the “middleware”. Primarily, this is the client software that runs on set–top boxes or web browsers. This is therefore almost entirely JavaScript.

Developing software that is primarily JavaScript is a new experience for me and one I‘m relishing, if with some trepidation. After a bit of Googling, I‘ve basically concluded that there isn‘t vast support for TDD for JavaScript development. This is especially true of IDE support. One thing that does look promising is JsTestDriver, though I‘ve been having trouble getting it to work reliably. This is most likely lack of familiarity with both JavaScript and JsTestDriver.

This is challenging stuff, but that‘s great as far as I‘m concerned.

First published on Jan 5, 2010. Last updated on: Jan 5, 2010.

Previewing text using JavaScript and AJAX

I recently added a new feature to the administration of the my site. Since I haven‘t implemented anything near a sophisticated publishing facility, I wanted a way of previewing my articles without having to save them. Fairly basic, but here is how I did it.

To help me along I wrote a simple JavaScript library to convert any textarea into previewable widget using AJAX to use a server-side script process the text. See the source code below. It depends on Prototype but you‘ll probably be able to modify it fairly easily to remove that dependency.

//    Copyright (c) 2008 Elwyn Malethan
//
//     This program is free software: you can redistribute it and/or modify
//     it under the terms of the GNU Lesser General Public License as published
//     by the Free Software Foundation, either version 3 of the License, or
//     (at your option) any later version.
//
//     This program is distributed in the hope that it will be useful,
//     but WITHOUT ANY WARRANTY; without even the implied warranty of
//     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//     GNU General Public License for more details.
//
//     You should have received a copy of the GNU Lesser General Public License
//     along with this program.  If not, see <http://www.gnu.org/licenses/>.


//---------------------------------------------------------------------- Constructor

function TextPreview(textarea, previewUrl) {
    this._textarea = textarea;
    this._previewUrl = previewUrl;

    this._textarea.addClassName("tp_source")
    var previewHtml = '<div class="tp_preview" id="' + this._textarea.id + 'PreviewPane"' +
                      ' style="' +
                      ' display: none;' +
                      ' width:' + this._textarea.clientWidth +'px;' +
                      ' height:' + this._textarea.clientHeight +'px;' +
                      '">' +
                      '</div>' +
                      '<div id="' + this._textarea.id + 'Toolbar" class="text_preview_tools clear"' +
                      ' style="width:' + this._textarea.clientWidth +'px;">' +
                      '<div class="tab source active" id="' + this._textarea.id + 'Source"' +
                      ' onclick="TextPreview.showSource(\'' + this._textarea.id + '\')"' +
                      '>Source</div>' +
                      '<div class="tab source" id="' + this._textarea.id + 'Preview"' +
                      ' onclick="TextPreview.showPreview(\'' + this._textarea.id + '\', \'' +this._previewUrl + '\')"' +
                      '>Preview</div>' +
                      '</div>';

    new Insertion.After(this._textarea, previewHtml)
}

//---------------------------------------------------------------------- Static Methods

TextPreview.showPreview = function(textareaId, previewUrl) {
    $(textareaId).hide();
    $(textareaId + 'PreviewPane').show();
    $(textareaId + 'Preview').addClassName("active");
    $(textareaId + 'Source').removeClassName("active");
    new Ajax.Updater(
            textareaId + 'PreviewPane',
            previewUrl,
            { method: 'post', parameters: 'source=' + escape($(textareaId).value) }
    );
}

TextPreview.showSource = function(textareaId) {
    $(textareaId).show();
    $(textareaId + 'PreviewPane').hide();
    $(textareaId + 'Source').addClassName("active");
    $(textareaId + 'Preview').removeClassName("active");
}

//---------------------------------------------------------------------- Instance Methods

TextPreview.prototype.showPreview = function() {
    TextPreview.showPreview(this._textarea, this._previewUrl)
}

TextPreview.prototype.showSource = function() {
    TextPreview.showSource(this._textarea)
}

To use it you just do something like this:

<html>
<head>
    <link href="text_preview.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript" src="text_preview.js"></script>
    <!-- ... -->
</head>
<body>
    <!-- ... -->
    <p><label>Body</label><br />
        <textarea id="blogPost_body" name="blogPost.body" cols="80" rows="20"></textarea>
    </p>

<script type="text/javascript">
    new TextPreview($('blogPost_body'), '... path to server side script ...' )
</script>
    <!-- ... -->
</body>
</html>

You‘ll also need some CSS like this below:

.tp_source, .tp_preview {
    border: 1px solid #999999;
    border-bottom: none !important;
    margin-top: 1px;
    margin-bottom: 0;
    padding: 5px;
    color: black;
    overflow-y: scroll;
    background-color: white;
}

.text_preview_tools {
    border-top: 1px solid #999999;
    padding: 0 5px 0 5px;
    font-size: 0.8em;
    clear:both;
    height: 2em; 
}

.text_preview_tools .tab {
    color: white;
    background-color: #999999;
    float: left;
    cursor: pointer;
    padding: 3px 5px 3px 5px;
    margin-left: 5px;
    margin-right: 5px;
    margin-top: -1px;
    height: 1.8em; 
}

.text_preview_tools .active {
    background-color: white;
    color: #999999;
    padding: 3px 4px 2px 4px;
    border: 1px solid #999999;
    border-top-color: white;
}

.text_preview_tools .tab:hover {
    text-decoration: underline;
}

Hope you enjoy! (Until I get comments implemented for this website. I‘ll just have to assume I have readers and that some of you did indeed enjoy… )

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

 
People I like
Other sites