Wednesday, July 18, 2012

NodeUnit, Mongoose and Grunt watch

Edit: Although interesting to me as a history to my Node.js testing issues, this article is now pretty much superseded by this one which better addresses all of the below problems

Now that James Shore's "Test Driven Javascript" series has kicked off I've been integrating unit tests into the 5Live hangout project. This has, for the most part, been simpler than I expected. NodeUnit is pretty easy to use and from one of the comment threads I have been introduced to Grunt which allows me to tie all my lint tasks and unit tests into a single automated script (James has been doing this himself in Jake but I figured I would give Grunt a try as it does some of the 'grunt' work for me :)) .

Like I said, for the most part this has all been going swimmingly. One of the nice features of Grunt that I discovered is the watch task. This allows me to watch a list of files and when they change, automagically kick off my lint tasks and unit tests - very nice :D

There are some problems though. My application uses Mongoose to interact with a MongoDB database. As such I follow the standard mongoose pattern of using a singleton and defining my model schemas like this...

As I'm doing TDD I actually start off with something like this in a separate test file...

That's all hunkydory. I can run the tests and they pass. I can kick off grunt watch and leave it running while I start editing my files. Let's see what happens when I change my test, thusly...

As expected grunt watch pings me to let me know that my test has failed :)

So I go back to my model and update the greeting function...

Gah, grunt watch pings me again to say that my test still fails. Puzzlement abounds!

If I stop grunt watch and run the tests manually they pass! So what's going on?

Well I wasted a lot of time messing around with the require.cache object, as I figured it was something to do with Node.js module caching, but that wasn't it at all. Either NodeUnit or Grunt is smart enough to remove the changed files from the module cache (I think it must be Grunt that does this but I didn't check).

Eventually I realised that it was the mongoose singleton that was causing the problem. After all this only happened with my mongoose model tests. As the mongoose singleton persists between test runs it doesn't matter that I change the methods on my models, the old versions also persist.

Again I tried a number of workarounds but so far the best seems to be the following.

First I created a wrapper for the mongoose singleton which allows me to reset the schemas...

Next I integrated this wrapper into my tests (only the tests, i still use the the mongoose singleton directly in the model implementations)...

So why do I prefer this solution and what else did I try?

Well I also had another workaround which fixed the problems with method updates.
  • Instead of using Schema.methods to assign methods I used Model.prototype
  • Instead of using Schema.statics to assign static methods I just assigned them to the Model directly
Why didn't I like this?
  • This solution meant a little rejigging of the code to what seemed like a non standard pattern in the actual implementations
  • This did not fix a similar problem with updating the actual Schema - ie. adding or removing fields
I still don't much like my eventual workaround as...
  • it depends on knowledge of the the internals of Mongoose (which might change)
But at least it's contained in my tests and seems to work for all changes in the model.

However, even with this workaround in place I'm still  not fully happy with the way grunt watch works.
  • Annoyingly, it exits on a number of test failures particularly when things are not yet defined. This happens a lot when doing TDD, it's how we write failing tests.
    • When it does exit it doesn't actually ping me. As such I have to keep looking to see if it stopped (If i have to do this all the time, it occurs to me that I may as well not use it and instead run my tests manually)
  • I'm now just waiting for the next gotcha as I only worked around a problem with Mongoose.
    • It seems to me quite likely that there will be other libraries that follow similar internal patterns and they are likely to trip me up in the same way
I have a solution to suggest though...
  • Spawn a new Node.js process at least for every grunt watch event if not for every NodeUnit test
    • Wouldn't this fix the problem once and for all?

No comments:

Post a Comment