Testing the to-do list

Now that we're done with the user registration and the session initiation, we are ready to test the core of our app, which is to manage to-do items. We will start by segregating that part of the application tests into a file of their own at test/todos.js, which may start with the following boilerplate:

var assert   = require('assert'),
    Browser  = require('zombie'),
    app      = require('../app'),
    couchdb  = require('../lib/couchdb'),
    dbName   = 'todos',
    db       = couchdb.use(dbName),
    fixtures = require('./fixtures'),
    login    = require('./login');

describe('Todos', function() {

  before(function(done) {
    app.start(3000, done);
  });

  after(function(done) {
    app.server.close(done);
  });

  beforeEach(function(done) {
    db.get(fixtures.user.email, function(err, doc) {
      if (err && err.status_code === 404) return done();
      if (err) throw err;
      db.destroy(doc._id, doc._rev, done);
    });
  });
});

Here we have similar boilerplate code for the other modules, with the difference that now we are dealing with a database named todos, not users. Another difference is that we want to start with a clean to-do list for each test, so we're adding a beforeEach hook that removes all the to-do items for the test user.

We are now ready to start carving out some tests, but there is at least one cumbersome repetitive task ahead that can be avoided at an early stage: logging in. We should assume that each test is individually reproducible and that the ordering of the tests doesn't matter—each test should rely on one browser instance, mimicking one individual user session per test. Also, since all the to-do item manipulation is scoped to a user and the user session must be initiated, we need to abstract that away into its own module inside test/login.js:

var Browser = require('zombie'),
    fixtures = require('./fixtures'),
    assert = require('assert'),
    couchdb = require('../lib/couchdb'),
    dbName  = 'users',
    db      = couchdb.use(dbName);


function ensureUserExists(next) {
  db.get(fixtures.user.email, function(err, user) {
    if (err && err.status_code === 404) {
      db.insert(fixtures.user, fixtures.user.email, next);
    }
    if (err) throw err;
    next();
  });
}


module.exports = function(next) {
  return function(done) {

    ensureUserExists(function(err) {
      if (err) throw err;
      Browser.visit("http://localhost:3000/session/new",
        function(err, browser) {
          if (err) throw err;

          browser
            .fill('E-mail', fixtures.user.email)
            .fill('Password', fixtures.user.password)
            .pressButton('Log In', function(err) {
              if (err) throw err;
              assert.equal(browser.location.pathname, '/todos');
              next(browser, done);
            });

        });
    });
  };
};

This module makes sure that a test user exists before loading, filling, and posting the user login form. After that it hands off the control to a next function.

Now we are ready to add further description scopes inside our todos scope. One of these scopes is the to-do list, which will have this code:

Here we can see that that we are making use of our login module to abstract away the session initiation dance, making sure our callback function only gets called once the user is logged in. Here we have three tests.

In our first test, named should have core elements, we are simply loading the empty to-do list and asserting that we have some elements in place, such as a heading containing the Your To-dos text and a link to create a new to-do item.

In the following test, named should start with an empty list, we are simply testing whether the to-do list contains zero elements.

In the last test of this scope, named should not load when the user is not logged in, we are asserting that this list is inaccessible to the user that has not yet initiated the session, making sure he is redirected to /session/new if we try to load the To-do list URL.

Now, we need to test whether the to-do items can really be created. For that, follow these steps:

  1. We need a new description scope that we'll name Todo creation form—which will be another subscope of the Todos one:
      describe('Todo creation form', function() {
  2. We can now test whether the to-do creation form is available for the user who is not logged in:
        it('should not load when the user is not logged in', function(done) {
          Browser.visit('http://localhost:3000/todos/new', function(err, browser) {
            if (err) throw err;
            assert.equal(browser.location.pathname, '/session/new',
              'should be redirected to login screen');
            done();
          });
        });

    Here we are verifying that the user gets redirected to the login screen if an attempt is made to load the to-do item creation form without being logged in.

  3. If the user is logged in, we check whether the page loads with some expected elements such as the title and the form elements for creating a new to-do item:
        it('should load with title and form', login(function(browser, done) {
          browser.visit('http://localhost:3000/todos/new', function(err) {
            if (err) throw err;
            assert.equal(browser.text('h1'), 'New To-Do');
            
            var form = browser.query('form');
            assert(form, 'should have a form');
            assert.equal(form.method, 'POST', 'form should use post');
            assert.equal(form.action, '/todos', 'form should post to /todos');
            
            assert(browser.query('textarea[name=what]', form),
              'should have a what textarea input');
            assert(browser.query('input[type=submit]', form),
              'should have an input submit type');
            
            done();
          });
        }));

    Here we are verifying that the form is present, that it has the necessary attributes to make a POST request to the /todos URL, and that the form has a text area input and a button to press.

  4. Now we can also test whether we can successfully create a to-do item by filling the respective form and submitting it:
        it('should allow to create a todo', login(function(browser, done) {
          browser.visit('http://localhost:3000/todos/new', function(err) {
            if (err) throw err;
    
            browser
              .fill('What', 'Laundry')
              .pressButton('Create', function(err) {
                if (err) throw err;
    
                assert.equal(browser.location.pathname, '/todos',
                  'should be redirected to /todos after creation');
    
                var list = browser.queryAll('#todo-list tr.todo');
                assert.equal(list.length, 1, 'To-do list length should be 1');
                var todo = list[0];
                assert.equal(browser.text('td.pos', todo), 1);
                assert.equal(browser.text('td.what', todo), 'Laundry');
    
                done();
    
              });
          });
        }));

    Here we are finally testing whether the form allows us to post a new item and whether the item gets created. We are doing that by loading and filling in the to-do item creation form; verifying that we've been redirected to the to-do item list page; and that this page contains the single to-do item that we've just created.

Now that we've tested to-do item insertion, we can test whether one can actually remove these items from one's list. We will place these tests inside a describe scope named Todo removal form, inside which we will test for two things: the removal of one to-do item when only one exists and the removal of a to-do item when more than one item exists.

Here is the code for the removal from a one-item list:

Before we run the test, there is a beforeEach hook that inserts a to-do item into the todo database for the test user. That's just one to-do item that's taken from fixtures.todo, which is a property we need to add to the test/fixtures.json file:

You may notice that, we're taking the opportunity here to add some additional fixtures that will help in future tests.

Continuing analyzing the test code, we see that the test fetches the to-do list and then verifies that the number of to-do items is actually one:

Then it goes on to try and press the remove button of that one to-do item:

The selector assumes that there is one to-do item on the table, which we had already verified before.

Then, after pressing the button and submitting the removal form, we're verifying that no errors occurred, that the browser was redirected back to the /todos URL, and that the presented list is now empty:

Now that we've tested that this works well for removing one item from a one-item list, let's create a more evolved test that asserts that we can remove a specific item from a list of three items:

This description scope will sit at the same level as the previous one, also inserting a document in the todo database, but this time the document contains a list of three to-do items, taken from the fixtures.todos attribute (instead of the previously used singular fixtures.todo attribute).

The test starts by visiting the todo list page and building a list of the expected to-do items, stored in the variable named expectedList. We then retrieve all the to-do list items found on the HTML document and verify that the content is what is expected:

Once we have verified that all the expected to-do items are in place and in order, we go on to click on the button for the second item on the list by using the following code:

Here, we're using the special CSS selector nth-child for selecting exactly the row for the second do-to item and then fetching the code for removing submit button inside it, and finally pressing it.

Once the button is pressed, the form is submitted, and the browser calls back, we verify that there are no errors, that we got redirected back to the /todos URL, and also that it contains the expected list. We do this last bit by removing the second element from the previously used expectedList array and verifying that this is exactly what is shown in the current page: