Yehuda Katz is a member of the Ember.js, Ruby on Rails and jQuery Core Teams; he spends his daytime hours at the startup he founded, Tilde Inc.. Yehuda is co-author of best-selling jQuery in Action and Rails 3 in Action. He spends most of his time hacking on open source—his main projects, like Thor, Handlebars and Janus—or traveling the world doing evangelism work. He can be found on Twitter as @wycats and on Github.

JSpec: BDD for JS

A couple of weeks ago I wrote a nice little piece of JS that implemented BDD-style testing in JS. It was originally 70 or so lines of code, and supported code like:

jspec.describe("Some stuff", function() {
  it("should rock", function() {
    7.should("==", 7);
  });
});

It was (and still is) a very simple piece of code, that only dumps the output to the Firebug console. Because should() was a simple method, I was able to delegate out to a pluggable jspec.matchers object, which is where I specified the “match” definition, and allowed the customization of a “failure message.” The original jspec had just two matchers (“==” and “include”), and allowed the creation of new ones by extending the jspec.matchers object (all JS, no magic).

When I tried to actually use it to test real-world code, though, some issues came up. First, functions were being dumped in full into the output, which was sucky. Secondly, the default describe syntax was sucking a bit for certain cases. And finally, I wanted you to be able to plug in any logger you wanted (for say, pretty HTML output). I added those new features, and am now releasing this quick and dirty code for people to look at.

Some examples

(which are included in the downloadable file)

jspec.describe("JSpec", function() {
  it("should support ==", function() {
    (1).should("==", 1);
    var arr = [];
    arr.should("==", arr);
    var obj = new Object;
    obj.should("==", obj);
    document.should("==", document);
  });

  it("should support include", function() {
    [1,2,3,4,5].should("include", 3);
    [1,2,3,4,5].should_not("include", 3);
    document.getElementsByTagName("div").should("include", document.getElementById("hello"))
  });
  
  it("should support exists", function() {
    document.should("exist");
  });
  
  jspec.matchers["have_tag_name"] = {
    describe: function(target, not) {
      return jspec.compress_lines(this) + " should " + (not ? "not " : "") + "have " + target + " as its tag name."
    },
    matches: function(target) {
      return (this.tagName && this.tagName == target) ? true : false;
    },
    failure_message: function(target, not) {
      return "Expected " + this.toString() + (not ? " not " : " ") + "to have " + target + " as its tag name," +
        " but was " + this.tagName;
    }
  };
  
  it("should support custom matchers", function() {
    document.getElementById("wrapper").should("have_tag_name", "DIV");
    document.getElementById("wrapper").should_not("have_tag_name", "SPAN");
    document.getElementById("wrapper").should("have_tag_name", "SPAN");          
  });
});

And the Output

In Firebug

JSpec
  - should support ==
    - 1 should equal 1 (PASS)
    - [] should equal [] (PASS)
    - [object Object] should equal [object Object] (PASS)
    - [object HTMLDocument] should equal [object HTMLDocument] (PASS)
  - should support include
    - 1,2,3,4,5 should include 3 (PASS)
    - 1,2,3,4,5 should not include 3 (FAIL)
      Expected [[1,2,3,4,5]] not to include 3
    - [object HTMLCollection] should include [object HTMLDivElement] (PASS)
  - should support exists
    - [object HTMLDocument] should exist. (PASS)
  - should support custom matchers
    - [object HTMLDivElement] should have DIV as its tag name. (PASS)
    - [object HTMLDivElement] should not have SPAN as its tag name. (PASS)
    - [object HTMLDivElement] should have SPAN as its tag name. (FAIL)
      Expected [object HTMLDivElement] to have SPAN as its tag name, but was DIV

Some caveats

  • Despite some work put in, I can’t figure out how to get jspec to recover gracefully from errors and continue on. This will work in a future release
  • I temporarily override the Object prototype (evil, I know) for the duration of the test. You will still be able to load in libraries that expect Object to work correctly, but you will not be able to test functions (such as prototype’s Object.extend) without accounting for the fact that Object.prototype has been extended.

Happy hunting (Download here)

Update

There is now a git repo for the project at git.caboo.se/jspec.git

7 Responses to “JSpec: BDD for JS”

Let’s get a repo going. I’m gonna mix it up with some mozrepl magick. http://hyperstruct.net/projects/mozrepl/

That example looks interesting.

I wonder why you use should(“include”, …) instead of should_include(…) or should(“==”, …) instead of should_equal(…). You’ve started adding to Object.prototype anyway, so why stop halfway through?

On a side note: What is BDD?

@Jörn: This way people can add to the matchers by just modifying the jspec.matchers object, instead of needing to define a new method on the Object prototype.

Also, while I have extended the Object prototype, I really don’t like it, and want to keep the number of extended objects finite and determinable (that way someone can still test jQuery.extend as long as they understand that should and should_not will be on the new Object erroneously).

How about returning an object from should() that provides the methods you want to use? Something like 7.should().equal(…) or document.should().exist().

That way you can keep the number of methods on Object.prototype low and still stick with methods instead of parameters.

How does this work with JQuery? Note: I’m not a JavaScript master yet.

I have a library named JSpec as well, far more robust :) and will top pretty much every javascript bdd framework out there once it hits 1.0

http://github.com/visionmedia/jspec/tree/master

Just released 1.0, check it out at http://visionmedia.github.com/jspec/

Leave a Reply

Archives

Categories

Meta