2011-04-14

Syntactic sugar for Vows.js

We end up writing quite a few full-stack integration tests. As the workflows for our projects get longer and more complex, the corresponding Zombie and Vows tests also get longer and more complex. Soon enough, a single test can be nested 15 or 20 levels deep, with a mix of anonymous functions and commonly-used macros.

Needless to say, these tests can become monsters to try and read through and understand:
var vows = require('vows');

vows.describe("Floozle Workflow").addBatch({
    "Go to login page" : {
        topic : function () {
            zombie.visit("http://widgetfactory.com", this.callback);
        },
        "then login" : {
            topic : function (browser) {
                browser
                  .fill("#username", "testuser")
                  .fill("#password", "testpass")
                  .pressButton("Login", this.callback);
            },
            "then navigate to Floozle listing" : {
                topic : function (browser) {
                    browser.click("Floozles", this.callback);
                },
                "should be on the Floozle listing" : function (browser) {
                    assert.include(browser.text("h3"), "Floozles");
                },
                "should have a 'Create Floozle' link" : function (browser) {
                    assert.include(browser.text("Create Floozle"));
                },
                "then click 'Create Floozle'" : {
                    topic : function (browser) {
                        browser.click("Create Floozle");
                    },
                    "then fill out Floozle form" : {
                        topic : function (browser) {
                            browser
                              .fill("#name", "Klaxometer")
                              .fill("#whizzbangs", "27")
                              .choose("#rate", "klaxes per cubic freep")
                              .pressButton("Save", this.callback);
                        },
                        // .... Continue on from there, with more steps
                    }
                }
            }
        }
    }
}).export(module);
So we created prenup, a syntactic sugar library for easily creating Vows tests. Prenup provides a fluent interface for generating easy to read test structures as well as the ability to reuse and chain testing contexts.

Here is the above test written with prenup:
var vows = require('vows'),
    prenup = require('prenup');

var floozleWorkflow = prenup.createContext(function () {
    zombie.visit("http://widgetfactory.com", this.callback);
})
.sub("then login", function (browser) {
    browser
      .fill("#username", "testuser")
      .fill("#password", "testpass")
      .pressButton("Login", this.callback);
})
.sub("then navigate to Floozle listing", function (browser) {
    browser.click("Floozles", this.callback);
})
.vow("should be on the Floozle listing", function (browser) {
    assert.include(browser.text("h3"), "Floozles");
})
.vow("should have a 'Create Floozle' link", function (browser) {
    assert.include(browser.text("Create Floozle"));
})
.sub("then click 'Create Floozle'", function (browser) {
    browser.click("Create Floozle");
})
.sub("then fill out Floozle form", function (browser) {
    browser
      .fill("#name", "Klaxometer")
      .fill("#whizzbangs", "27")
      .choose("#rate", "klaxes per cubic freep")
      .pressButton("Save", this.callback);
})
.root();

vows.describe("Floozle Workflow").addBatch({
    "Go to login page" : floozleWorkflow.seal()
}).export(module);
Every `sub()` call generates a new sub-context, and every `vows()` call attaches a vow assertion to the most recent context. `parent()` can be called to pop back up the chain and attach parallel contexts. If a context is saved, it can be reused in other chains. When `seal()` is called, it will generate the same structure as the original test.

More examples, including branching and reusing contexts, are given in the documentation. Prenup is available on NPM via `npm install prenup`

No comments:

Post a Comment