In my spare time, I've recently been working with a few codebases that either are written in, or use enough code written nodejs, that make me keen to have some kind of testing framework in place to help put the same kinds of safety nets in place that I'm used to working with on Chef, Sinatra or Rails projects.
After losing a couple of weekends to trying to find an approach to BDD style development with node, and getting my head around asynchronous coding concepts, I think I've settled on an approach that feels enough like rspec to feel comfortable enough to use for future serverside js development.
It's way too much for a single post, to I'll be sharing the first of a three part series of posts, to help other Ruby developers used to synchronous development with rspec, adjust to asynchronous development, with the closest thing I can find to rspec right now, mocha.
I'll cover how Mocha syntax compares to rspec, then I'll cover implementing the code to pass these mocha specs, then I'll add a post to help keep asynchronous code halfway manageable in node.
I'm going to use a side project I've been hacking on for a few months to show how I'd add a new class, to wrap its calls to a persistence layer, to provide a degree of encapsulation, abstracting away the database technology from external interface for the class.
Lets say my all I want to do here with a User class here is have a method that finds me a user, stored as a hash in Redis, keyed on their machine's mac addess.
The tests I'd write might look a bit like this pseudocode here:
describe 'User' do
before(:each) do
redis = Redis.new
redis.hmset("99:aa:44:33:01:3r", {
:username => "mrchrisadams",
:name => "Chris"
:email => "[email protected]",
:mac_address => "99:aa:44:33:01:3r"
})
end
it 'fetches the user object' do
u = User.new
c = u.find_by_mac('99:aa:44:33:01:3r')
c.name.should be('mrchrisadams')
end
end
I use the before
block to store a hash inside redis, setting a few extra values on it, and then later on, in the it 'fetches the user object'
block, I instantiate an instance of my user class, and call the find_by_mac
method to fetch me the hash I just stored in Redis.
The implemntation code in Ruby, might look like this:
class User do
def initialize
@db = Redis.new
end
def find_by_mac(mac)
@db.hgetall(mac)
end
end
So far so good - this is synchronous code - it feels comfortable, and is easy enough to work with.
Now, lets try to take the same approach in node, to see how different this looks, but also to see what we need to be aware of when learning to think in asynchronous terms. the completed mocha test code is here on github, and the completed implementation code is here too
So, lets be good developers and try to write out test code first, in Mocha, the javascript flavoured take on rspec. I'll paste the lost, then go through the interesting bits piece by piece.
describe('User', function() {
describe('#findByDevice', function() {
beforeEach(function(done) {
db.hmset("mrchrisadams", {
name: "Chris Adams",
username: "mrchrisadams",
devices: ["00:1e:c2:a4:d3:5e"],
email_address: "[email protected]"
}, done);
})
it('should fetch the user for that mac', function(done) {
var user = new User();
user.findByDevice('00:1e:c2:a4:d3:5e', function(err, res) {
if (err) {
console.log(err)
} else {
// console.log(res)
res.username.should.be.ok
res.username.should.equal('mrchrisadams')
done()
}
}); // find by device
}) // should fetch the user for that mac
})
})
So first of all, much of the syntax is somewhat familiar. We have nested describe
and it
blocks, and event the assertion syntax is reassuringly familiar with nice, readable should
s, be
s and equal
s around.
However, there are a few important additions here that we need to allow for the asynchronous nature of node. First of all, lets look at the beforeEach
function.
beforeEach(function(done) {
db.hmset("mrchrisadams", {
name: "Chris Adams",
// object vars edited out for brevity
}, done);
})
In this case, we're making a call to the node-redis
a popular redis library for node, that is completely asynchronous. We could have tried usingthe bog-standard beforeEach
function like this, when working with an asynchronous library (not the lack of done
):
beforeEach(function() {
db.hmset("mrchrisadams", {
name: "Chris Adams",
// object vars edited out for brevity
});
})
Had we done this, node would have zipped to the beforeEach
function, started it, then returned straight away, racing ahead trying to run the tests below it, without waiting for our Redis setup steps to be finished. Now Redis is fast, but you can't rely on that to make sure your tests are set up before you run them, and this code would have given us at best unpredictable results, but more likely, fails across the board.
Here's how we do it when working with async library:
beforeEach(function(done) {
db.hmset("mrchrisadams", {
name: "Chris Adams",
// object vars edited out for brevity
}, done);
})
The difference this time round is that we're passing in done
, a function that exists to stop the tests running until Redis setup steps are finished, and we're in a state for testing.
We have to take this approach for the tests itself in our it
function:
it('should fetch the user for that mac', function(done) {
var user = new User();
user.findByDevice('00:1e:c2:a4:d3:5e', function(err, res) {
if (err) {
// do something to recover
} else {
res.username.should.be.ok
res.username.should.equal('mrchrisadams')
done()
}
}); // close findByDevice
}) // close should fetch the user for that mac
Here, we're doing something very different to the ruby approach of storing returned values from methods in variables, then testing the value of those.
Look at this line in paticular:
user.findByDevice('00:1e:c2:a4:d3:5e', function(err, res)
Understanding this here for me was the key to getting my head around this initially very alien syntax, and if you're having trouble with the shift from sync to async, the closest thing in typical sync ruby code might be something like, where you set the varibale res
then use it for testing assertions against:
res = findByDevice('00:1e:c2:a4:d3:5e)
res.should be_okay
Now there's two important things here to remember when working with node:
- Because we're working asynchronously, we only want to run out assertions once we know we have the values back from the call we just made
- We we're working with javascript, we can pass functions around to execute the code inside them, at a later date.
So, our solution to the asynchronous problem, is to pass in a function with the assertions we care about inside it, as a parameter to our findByDevice
call on the user object.
So, what we're saying here is, "go fetch the results of findByDevice
, with the paramters 00:1e:c2:a4:d3:5e
, and here's the function I'd like you to execute when you're done, please":
user.findByDevice('00:1e:c2:a4:d3:5e', function(err, res) {
if (err) {
// do something to recover
} else {
res.username.should.be.ok
res.username.should.equal('mrchrisadams')
done()
}
})
You might be confused by the two paramters err
, and res
. These are generally accepted convention when coding asynchronously in node, to make it possible to pass the result from one call back to another. Passing in a function which itself has the parameters error
, and result
(or some variaton on the name) as the last argument going into a method call is a generally accepted convention with node now, and is often referred to as the continuation-passing style. It's crucial to understand it, because you won't get far without it.
You might notice a call to done()
on the last line of the function we're passing in, after the assertions. Mocha, when you pass in done
to a testing block, doesn't known when the test is passed or failed, so will wait for done()
to be called, before deciding if that particular it
testing block has failed or passed.
So we've run thorugh how Mocha works now, and how it compares to Rspec, and we've seen how we rely on anonymmous functions run our assertions on the results of asynchronous functions. (i.e. anonymous functions are functions with no name, just the keyword, parameters, the code to execute like - function(err, res) { // do stuff }
).
It's worth re-reading the above, until you're really comfortable with the concepts, as the next section is unfortunately pretty messy.