Skip to content

Instantly share code, notes, and snippets.

@lackac
Created June 6, 2011 15:59
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lackac/1010528 to your computer and use it in GitHub Desktop.
Save lackac/1010528 to your computer and use it in GitHub Desktop.
vows + should.js + sinon.js (presentation for budapest.js, June 6, 2011)
vows.describe("Presentation")
.addBatch
"László Bácsi from Secret Sauce Partners, Inc.":
topic: "vows + should.js + sinon.js"
"should vow to do BDD": (tools) ->
tools.should.include "vows"
tools.should.include "should"
tools.should.include "sinon"
.export(budapest.js)

Vows

Asynchronous behaviour driven development for Node.

Structure

  • A Suite is an object which contains zero or more batches, and can be executed or exported.
  • A batch is an object literal, representing a structure of nested contexts.
  • A context is an object with an optional topic, zero or more vows and zero or more sub-contexts.
  • A topic is either a value or a function which can execute asynchronous code.
  • A vow is a function which receives the topic as an argument, and runs assertions on it.

Batches are run in sequence, but contexts within a batch are run in parallel (taking dependencies into account).

The Topic

  • can be a static value
{ topic: 42,
  'should be equal to 42': function(topic) {
    assert.equal(topic, 42);
  }
}
  • can come from the return value of a function
{ topic: function() { return 42 },
  'should be equal to 42': function(topic) {
    assert.equal(topic, 42);
  }
}
  • has access to all the parent topics
{ topic: new(DataStore),
  'should respond to `get()`': function(store) {
    assert.isFunction(store.get);
  },
  'calling `get(42)`': {
    topic: function(store) { return store.get(42) },
    'should return the object with id 42': function(topic) {
      assert.equal(topic.id, 42);
    }
  }
}
  • can be asynchronous
{ topic: function() { fs.stat('~/FILE', this.callback); },
  'can be accessed': function(err, stat) {
    assert.isNull(err);     // We have no error
    assert.isObject(stat);  // We have a stat object
  },
  'is not empty': function(err, stat) {
    assert.isNotZero(stat.size); // The file size is > 0
  }
}

Note: Beware the return value!

  • can be a promise too

should.js

should is an expressive, readable, test framework agnostic, assertion library for node.

Example

var user = { name: 'tj', pets: ['tobi', 'loki', 'jane', 'bandit'] };

user.should.have.property('name', 'tj');
user.should.have.property('pets').with.lengthOf(4)

someAsyncTask(foo, function(err, result) {
  should.not.exist(err);
  should.exist(result);
  result.bar.should.equal(foo);
});

Assertions

Has all the assertions you would want:

exist, ok, true, false, arguments, empty, eql, equal, within, a, instanceof, above, below, match, length, string, object, property, ownProperty, contain, keys, respondTo

and you can also extend it easily.

sinon.js

Standalone test spies, stubs and mocks for JavaScript. No dependencies, works with any unit testing framework.

Example

"test should call subscriber": function () {
  var spy = sinon.spy();

  PubSub.subscribe("message", spy);
  PubSub.publishSync("message", undefined);

  assertTrue(spy.called);
}

Sandboxes

Sandboxes simplify working with fakes that need to be restored and/or verified.

"test using sinon.test sandbox": sinon.test(function () {
    this.mock(API).expects("method").once();

    PubSub.subscribe("message", API.method);
    PubSub.publishSync("message", undefined);
})

Using batches to control application state

  • batches are run sequentially
  • if a test has to be run in a certain state, create that state in a previous batch
vows.describe("Model")
  .addBatch
    ".create":
      topic: -> Model.create({id: "foo", bar: 42}, this.callback)
      "should be saved": (doc) ->
        doc.rev.should.be.a "string"

  .addBatch
    ".find":
      topic: -> Model.find("foo", this.callback)
      "should return the document from the database": (doc) ->
        doc.bar.should.eql 42

Macros

Topics and contexts are just simple javascript objects which can be generated too. Create macros for repeating patterns.

vows.describe("API")
  .addBatch
    'GET /':
      topic: api.get('/')
      'should respond with a 200 OK': assertStatus(200)
    'POST /':
      topic: api.post('/')
      'should respond with a 405 Method not allowed': assertStatus(405)

The macros that makes this possible:

var api = {
  get: function(path) {
    return function() {
      client.get(path, this.callback);
    };
  }
};

function assertStatus(code) {
  return function(err, res) {
    assert.equal(res.status, code);
  };
}

More complex macros

vows
  .describe('API root')
  .addBatch
    'GET /': api.request()
      .returnsStatus(200)
      .sendsHeaders({ Server: "WaterCooler/#{App.version}" })
      .respondsWithJSON({ water_cooler: "Welcome", version: App.version })

Macro implementation

Create complete batches

A more generalized solution for controlling application state.

vows.describe("Some API")
  .initializeDB [
    { _id: 'cooler-1500', min_temperature: -42, type: 'Cooler' }
    { _id: 'cooler-2000', min_temperature: -65, type: 'Cooler' }
    { _id: 'order-ae452ff31c', line_items: ['cooler-2000'], type: 'Order' } ]
  .addBatch
    # ...

Macro implementation

api =
request: (headers, body) ->
new RequestContext(headers, body)
class RequestContext
constructor: (headers={}, body=null) ->
@topic = ->
[method, path, blurb...] = @context.name.split(' ')
sendRequest method, path, headers, body, @callback
return
returnsStatus: (code) ->
this["should return status code #{code}"] = (res) ->
res.should.have.status code
this
sendsHeaders: (pairs) ->
this["should send headers #{JSON.stringify(pairs)}"] = (res) ->
for key, value of pairs
res.should.have.header key, value
this
respondsWithJSON: (object) ->
this["should respond with JSON #{JSON.stringify(object)}"] = (res) ->
res.json_body.should.eql object
this
sendRequest = (method, path, headers, body, callback) ->
retry = -> sendRequest(method, path, headers, body, callback)
if App.__backlog
App.__backlog.push(retry)
return
unless App.__listening
App.__backlog = [ retry ]
App.listen 7357, ->
App.__listening = true
backlog = App.__backlog
delete App.__backlog
request() for request in backlog
return
if body?
if typeof body is "object"
body = JSON.stringify(body)
headers['Content-Type'] = "application/json"
options =
method: method
host: 'localhost'
port: 7357
path: path
headers: headers
req = http.request options, (res) ->
resp_body = ""
res.on "data", (chunk) -> resp_body += chunk
res.on "end", ->
res.body = resp_body
try
res.json_body = JSON.parse(resp_body)
catch e
callback(null, res)
res.on "error", (err) ->
callback(err)
req.write(body) if body
req.end()
req
{Suite} = require "vows/suite"
Suite::initializeDB = (docs=[]) ->
@addBatch
'initialize database':
topic: -> DB.destroy => DB.create(@callback)
done: (db) -> db.should.be.a 'object'
if docs.length > 0
createDocs =
topic: ->
async.map docs,
(doc, cb) -> DB.create(doc, cb)
@callback
return
for doc in docs
createDocs[JSON.stringify(doc)] = (saved_docs) ->
saved = false
# check if saved_docs includes doc
saved.should.be.true
@addBatch
'create documents to test with': createDocs
else
this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment