Node Tutorial Part 11

31 Jan 2011 | By Alex Young | Tags server node tutorials lmawa nodepad npm express testing

Welcome to part 11 of Let’s Make a Web App, a tutorial series about building a web app with Node. This series will walk you through the major areas you’ll need to face when building your own applications. These tutorials are tagged with lmawa.

Previous tutorials:

Checking Out Each Commit

You might have noticed I include a fragment of the SHA1 of each source code commit, so each part of the tutorial can be downloaded. A reader got in touch with us to ask how to actually do this. It’s this simple:

git clone git://github.com/alexyoung/nodepad.git
git checkout -b tutorial_8 841a49

This will create a new local branch with the commit at 841a49. The part that says -b tutorial_8 just gives the branch a name, so you could check out each commit (or any that you’re interested in) and easily switch between them.

Bugs and Improvements

Over the weekend, Matthias Lübken helped me figure out some odd behaviour in Nodepad. We had quite a lengthy discussion through GitHub and Twitter, and found that Nodepad doesn’t run too well on Node 0.3.x, so I recommend using 0.2.4 if possible.

On DailyJS we recently featured n which is a Node version manager — you could use it to switch between 0.2.4 and the 0.3 series. Another tool that can do this is nvm.

In addition, Matthias found that Jade’s latest version had a slight change in behaviour that caused Nodepad’s templates to generate escaped HTML from flash messages by mistake, so that’s now fixed as well.

Matthias also suggested that the app should use a session secret for added security. You should use the secret option for express.session for added security if possible.

Tests with Expresso and Zombie.js

The original basic tests I wrote no-longer work, which is partially my fault and partially due to a lack of good resources on testing with Expresso. Like most of our readers, I’m comfortable with server-side JavaScript and client-side tools, which made me wonder if it would be easier to write tests in a style that reflects that. Therefore, I decided to combine Expresso with Zombie.js.

So I went on a quest to write idiomatic Node test code with Expresso, Zombie.js, and Mongoose. The initial skeleton I created looks like this:

// Force test environment
process.env.NODE_ENV = 'test';

var app = require('../app'),
    path = require('path'),
    fs = require('fs'),
    assert = require('assert'),
    zombie = require('zombie'),
    events = require('events'),
    testRunner = require('./runner'),
    models;

// Mongoose models defined in the app -- I actually export them as app.User, etc.
models = ['User'];

function removeTestData(models, next) {
  // Clear test data
}

(function() {
  // The app needs to run for Zombie to test it
  app.listen(3001);

  // Clear tests on each run
  removeTestData(models, function() {
    // Fixtures
    var user = new app.User({'email' : 'alex@example.com', 'password' : 'test' });
    user.save(start);
  });
})();

function teardown() {
  removeTestData(models, function() {
    process.exit();
  });
}

function start() {
  exports['test login'] = function() {
    // The Zombie code goes here
  };
}

A self-executing anonymous function starts everything off. It runs the app on a different port to the development app, clears the test data, then creates a user. This is asynchronous, so if we had more fixtures, the very last save should call the start() function. Expresso will wait around for tests to be exported, so this allows us to use Mongoose’s asynchronous API and run tests when we’re ready.

If you understand that then take a look at the full test code:

// Force test environment
process.env.NODE_ENV = 'test';

var app = require('../app'),
    path = require('path'),
    fs = require('fs'),
    assert = require('assert'),
    zombie = require('zombie'),
    events = require('events'),
    testRunner = require('./runner'),
    models;

models = ['User'];

function removeTestData(models, next) {
  var modelCount = models.length;
  models.forEach(function(modelName) {
    modelCount--;
    app[modelName].find().all(function(records) {
      var count = records.length;
      records.forEach(function(result) {
        result.remove();
        count--;
      });
      if (count === 0 && modelCount === 0) next();
    });
  });
}

(function() {
  // The app needs to run for Zombie to test it
  app.listen(3001);

  // Clear tests on each run
  removeTestData(models, function() {
    // Fixtures
    var user = new app.User({'email' : 'alex@example.com', 'password' : 'test' });
    user.save(start);
  });
})();

function teardown() {
  removeTestData(models, function() {
    process.exit();
  });
}

function start() {
  exports['test login'] = function() {
    zombie.visit('http://localhost:3001/', function(err, browser, status) {
      // Fill email, password and submit form
      browser.
        fill('user[email]', 'alex@example.com').
        fill('user[password]', 'test').
        pressButton('Log In', function(err, browser, status) {
          // Form submitted, new page loaded.
          assert.equal(browser.text('#header a.destroy'), 'Log Out');
          teardown();
        });
    });
  };
}

The Zombie.js code visits the URL (notice it’s port 3001), then uses the browser object to fill out the login form, submit it, then use a standard assert.equal assertion to compare the text node to the string 'Log Out'.

Making it Generic

If we want to separate Nodepad’s tests into multiple files, repeating all of that Mongo fixture code would be poor form, so I’ve tried to make it more generic. This is my test/helper.js file:

// Force test environment
process.env.NODE_ENV = 'test';
var state = {
  models: []
};

function prepare(models, next) {
  var modelCount = models.length;
  models.forEach(function(model) {
    modelCount--;
    model.find().all(function(records) {
      var count = records.length;
      records.forEach(function(result) {
        result.remove();
        count--;
      });
      if (count === 0 && modelCount === 0) next();
    });
  });
};

module.exports = {
  run: function(e) {
    for (var test in state.tests) {
      e[test] = state.tests[test];
    }
  },

  setup: function(next) {
    prepare(state.models, next);
  },

  end: function() {
    prepare(state.models, process.exit);
  },

  set models(models) {
    state.models = models;
  },

  set tests(tests) {
    state.tests = tests;
  }
};

The environment is set to test because my default Nodepad settings have a test database defined which means it should be safe to trash data each time a set of tests is run. The prepare function just loops through each Mongoose model and removes associated data (this is reused from the previous example).

Now the tests are a little bit more concise:

var app = require('../app'),
    assert = require('assert'),
    zombie = require('zombie'),
    events = require('events'),
    testHelper = require('./helper');

app.listen(3001);

testHelper.models = [app.User];

testHelper.setup(function() {
  // Fixtures
  var user = new app.User({'email' : 'alex@example.com', 'password' : 'test' });
  user.save(testHelper.run(exports));
});

testHelper.tests = {
  'test login': function() {
    zombie.visit('http://localhost:3001/', function(err, browser, status) {
      // Fill email, password and submit form
      browser.
        fill('user[email]', 'alex@example.com').
        fill('user[password]', 'test').
        pressButton('Log In', function(err, browser, status) {
          // Form submitted, new page loaded.
          assert.equal(browser.text('#header a.destroy'), 'Log Out');
          testHelper.end();
        });
    });
  }
};

Did you notice I used a setter for setting tests and models? I thought that was an interesting trick so I left it in there.

Conclusion

When writing Expresso tests it’s important to remember that it’s possible to delay the running of tests by deferring setting them in module.exports. TJ’s example in the Expresso documentation uses setTimeout, but keeping a callback floating also works.

If you run my code (remember to use expresso), you should see this:

alex@mog ~/Code/nodepad[git:master]$ expresso test/app.test.js 
GET / 2 ms
GET /sessions/new 10 ms
GET /javascripts/application.js 1 ms
GET /javascripts/json.js 1 ms
POST /sessions 2 ms
GET /documents 8 ms
GET /javascripts/application.js 1 ms
GET /javascripts/json.js 1 ms
GET /documents/4d21d96f2410f20000000037.json 5 ms

   100% 1 tests

Zombie.js (or Tobi) is a very JavaScript developer friendly way to test, but you still need some kind of test runner. A lot of people are using Vows, and I’m sure nodeunit would work well too.

I found it disappointing that there aren’t many open source Express apps with solid tests. Also, writing tests without a convenient way of handling setup, teardown, and fixtures seems awkward to me.

You should now be able to at least run the Nodepad tests. Hopefully next week I’ll be able to write more detailed Zombie.js tests so we can really push what we’ve created so far.

This version is commit 6a269ce.

References


blog comments powered by Disqus