DailyJS

Node Tutorial Part 11

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials server testing node lmawa nodepad npm express

Node Tutorial Part 11

Posted by Alex R. Young on .
Featured

tutorials server testing node lmawa nodepad npm express

Node Tutorial Part 11

Posted by Alex R. Young on .

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