Jorge Ramon

  • Home
  • New Here?
  • Articles
    • All
    • Sencha Touch
    • jQuery Mobile
    • ExtJS
  • Books
  • About

Mocha and Node JS – Testing the Backend for a Mobile Application

December 19, 2014 11 Comments

This Mocha and Node JS tutorial shows you how to test a Node.js and MongoDB backend for a mobile application. This article is part of a series of mobile application development tutorials that I have been publishing on my blog jorgeramon.me, where we are creating a Meeting Room Booking mobile application with a Node.js and MongoDB backend.

featured-1

In the previous article of this series, we built the server-side AccountController module that will handle the user registration, login and logout features of the application. We implemented the AccountController’s public interface, along with a number of helper Classes that will allow the Controller to do its work. Our goal for this article is to create a set of unit tests for the Controller’s public interface (public methods) that will prevent us from breaking the application if we need to refactor the Controller in the future.

An extended version of this end-to-end tutorial will be included in my upcoming Mobile Web Apps Recipes Book, that will show you how to develop 8 different mobile applications using Ionic, Sencha Touch, Kendo UI Mobile and jQuery Mobile.

The test framework that we will use in this article is Mocha. Please install it on your development environment before moving on. You can consult Mocha’s installation instructions for a seamless install experience.

We are also going to use Should in our tests. Should will help us create assertions with Behavior Driven Development (BDD) style. You can find installation instructions for the library in its homepage.

Creating a Mock for a Mongoose Model

We created the User Class in the preceding article of this series. This Class is a Mongoose model that we will use to hold the user’s attributes needed by the mobile application. In that article we also talked about the Mailer Class, which we have not created yet. Mailer will be used by the AccountController Class to handle the sending of the “password reset” emails the users of the application.

You might recall that we designed the AccountController module so the User and Mailer instances it depends on are passed to its constructor as arguments, along with a session object:

var AccountController = function (userModel, session, mailer) {…}

In our unit tests, we will feed the Controller’s constructor mock instances of the User and Mailer Classes, as there are specific properties and methods on those Classes that the Controller invokes. We will also pass a JavaScript Object for the session argument, as the Controller uses the session to store arbitrary values.

From the previous article of this series you might also remember that the User Class is a Mongoose Model. Our mock of the User Model Class will need to implement the methods of the User Class that the AccountController Class invokes. Let’s start building this mock step by step.

First, we will create the test directory inside the server directory, and then create the user-mock.js file:

directories-10

In the user-mock.js file, we are going to define the UserMock Class as follows:

var UserMock = function () {

    this.uuid = require('node-uuid');
    this.crypto = require('crypto');
    this.User = require('../models/user.js');
    this.seedUsersCount = 10;
    this.users = [];
    this.err = false;
    this.numberAffected = 0;
};

The uuid and crypto variables refer to the node-uuid and Node.Crypto modules. As I mentioned in the previous article of this series, we will use them to generate password hashes and unique identifiers. We are also importing the User Model so we can create User instances for testing purposes.

During our tests we will simulate an instance of the MongoDB Users collection with the users Array. The seedUsersCount variable defines the total number of test users that we will hold in the Array. The err and numberAffected properties are helper variables that we will use in our tests. They will help us simulate error conditions in the MongoDB database.

Before we implement the methods that the User Class inherits from Mongoose, let’s take care of a few helper methods. The first two are setter methods for the err and numberAffected properties:

UserMock.prototype.setError = function (err) {
    this.err = err;
};

UserMock.prototype.setNumberAffected = function (number) {
    this.numberAffected = number;
};

We will also add a method to seed the users Array, which is our test users collection, with User Class instances:

UserMock.prototype.seedUsers = function () {
    for (var i = 0; i < this.seedUsersCount; i++) {

        var passwordSaltIn = this.uuid.v4(),
            cryptoIterations = 10000, // Must match iterations used in controller#hashPassword.
            cryptoKeyLen = 64,       // Must match keyLen used in controller#hashPassword.
            passwordHashIn;

        var user = new this.User({
            email: 'Test' + i + '@test.com',
            firstName: 'FirstName' + i,
            lastName: 'LastName' + i,
            passwordHash: this.crypto.pbkdf2Sync('Password' + i, passwordSaltIn, cryptoIterations, cryptoKeyLen),
            passwordSalt: passwordSaltIn
        });

        this.users.push(user);
    }
};

And last, a method that returns one of the User Class instances in the database so we can use it for testing purposes:

UserMock.prototype.getTestUser = function () {
    return this.users ? this.users[0] : null;
};

Now we can create the methods inherited from Mongoose. First, let add the findById method, which mocks the behavior of Mongoose’s findById method:

UserMock.prototype.findById = function (id, callback) {
        
    for (var i = 0, length = this.users.length; i < length; i++) {

        if (this.users[i]._id === id) {
            return callback(this.err, this.users[i]);
        }
    }        

    return callback(this.err, null);
};

This method takes the id of the model to find and a callback function as arguments. It does is iterate over the users Array and invokes the callback function when the id of User Class instance in the Array matches the supplied id. If there is no User instance with an id that matches the provided id, the method invokes the callback and passes the err variable to it.

Let’s also add the findOne method, which mocks Mongoose’s findOne method:

UserMock.prototype.findOne = function (where, callback) {

    for (var i = 0, length = this.users.length; i < length; i++) {
            
        if (this.users[i].email === where.email) {
            return callback(this.err, this.users[i]);
        }
    }

    return callback(this.err, null);
};

The Mongoose version of this method passes to its callback argument the first model instance that meets the where condition. Our version of the method will not accept an arbitrary condition. It will need the condition to have an email property, and will pass to its callback argument the first User model instance whose email property matches the condition’s email. We are making this simplification because the AccountController Class only invokes the User Model’s findOne method with conditions that evaluate the email property of the Model.

The third Mongoose-inherited method that we will add to our mock Class is save. This is its implementation:

UserMock.prototype.save = function (callback) {
    return callback(this.err, this, this.numberAffected);
};

In a Mongoose Model (the User Class), the save method saves the model and invokes the provided callback. If the save operation succeeded, the numberAffected value is set to 1. Our save method in the UserMock Class will simply invoke the callback. We don’t need to actually save anything, as we can simulate success or failure by setting the UserMock numberAffected variable to 1 or 0.

We will take the same approach with the update method, which the User Class also inherits from Mongoose. Let’s add it to UserMock:

UserMock.prototype.update = function (conditions, update, callback) {
    return callback(this.err, this.numberAffected);
};

Here we can also simulate success or failure by setting the mock’s numberAffected variable to 1 or 0.

Finally, let’s export the module:

module.exports = UserMock;

Creating a Mock for the Mailer Class

The AccountController Class delegates to the Mailer Class the responsibility of sending password reset email notifications. We will use the MailerMock Class to mock this functionality. Let’s create the mailer-mock.js file in the test directory:

directories-11

In the file, let’s add the Class’s implementation as follows:

var MailerMock = function () {  };

MailerMock.prototype.sendPasswordResetHash = function (email, passwordResetHash) { };

module.exports = MailerMock;

The Class has only one method, sendPasswordResetHash, which we will leave empty because we just need it to exist in order for the AccountController Class to be able to invoke it.

Defining a Test Suite in Mocha

Now we can go ahead and start creating the Mocha tests for the AccountController Class. We need a file to place the tests in so let’s create the account-controller-test.js file in the test directory:

directories-12

We will start with importing the modules needed to run the tests:

var AccountController = require('../controllers/account.js'),
    mongoose = require('mongoose'),
    should = require('should'),
    uuid = require('node-uuid'),
    crypto = require('crypto'),
    User = require('../models/user.js'),
    UserMock = require('./user-mock.js'),
    MailerMock = require('./mailer-mock.js'),
    ApiMessages = require('../models/api-messages.js');

We are importing the Mongoose, Should, Node-uuid and Crypto libraries, along with the User, UserMock, MailerMock and ApiMessages Classes.

Next, we are going to define the ‘AccountController’ test suite:

describe('AccountController', function () {

});

Within the suite we will define the following helper variables:

var controller,
    seedUsersCount = 10,
    testUser,
    userModelMock,
    session = {},
    mailMock;

The controller, testUser, userModelMock and mailMock variables will hold references to the User, UserMock and MailMock Classes respectively. The seedUsersCount variable represents the maximum number of User instances that we will create for testing purposes. We will use the session variable to feed test session data the AccountController instance under test.

Creating Test Pre-Conditions and Post-Conditions with Mocha Hooks

Mocha provides the following hook functions that run at different times within the lifecycle of a test suite:

  • before() runs before all tests in the suite where it is defined
  • after() runs after all tests in the suite where it is defined
  • beforeEach() runs before each test in the suite where it is defined
  • afterEach() runs after each test in the suite where it is defined

We are going to take advantage of the beforeEach hook to initialize the AccountController instance that we intend to test:

beforeEach(function (done) {
    userModelMock = new UserMock();
    mailerMock = new MailerMock();
    controller = new AccountController(userModelMock, session, mailerMock);
    done();
});

Note that we are also invoking the Mocha’s done method before exiting beforeEach. The done function is a convention used in Mocha to let the framework know that it needs to wait for any asynchronous code in the body of the method containing the call to done before moving on to the next execution step. We take this approach because the creation of UserMock, MailerMock and AccountController instances involve the download of their dependencies (via the require function), which might be a time-consuming process. We want to wait until this process completes before exiting the beforeEach function.

Similarly, we will we will use the afterEach hook to perform some cleanup:

afterEach(function (done) {
    userModelMock.setError(false);
    done();
});

Our cleanup code consists of resetting the userModelMock’s internal error variable so the next tests starts with a UserMock instance that is not in an error state.

As it is, we are ready to execute this test suite with Mocha. Let’s open a command window and navigate to the path of the server directory. Once there, we can run the mocha command, which should produce a result similar to the picture below:

tests-account-controller-2

When we run the command, Mocha will look for the test directory and run the test suites in any of the JavaScript files in the directory. This is how Mocha will execute the “AccountController” test suite in the account-controller-test.js file.

The green text in the result means that there were no errors while running the test. The message of the result explains that there are no tests passing. It makes sense, because we still haven’t written any tests.

Writing Mocha Tests for a Node.js Module

Testing the logon method

The first test that we will write targets the AccountController Class’s logon method. Here’s the method’s implementation that we created in the prior article of this series:

AccountController.prototype.logon = function(email, password, callback) {

    var me = this;

    me.userModel.findOne({ email: email }, function (err, user) {

        if (err) {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
        }

        if (user) {

            me.hashPassword(password, user.passwordSalt, function (err, passwordHash) {

                if (passwordHash == user.passwordHash) {

                    var userProfileModel = new me.UserProfileModel({
                        email: user.email,
                        firstName: user.firstName,
                        lastName: user.lastName
                    });

                    me.session.userProfileModel = userProfileModel;

                    return callback(err, new me.ApiResponse({
                        success: true, extras: {
                            userProfileModel:userProfileModel
                        }
                    }));
                } else {
                    return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.INVALID_PWD } }));
                }
            });
        } else {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.EMAIL_NOT_FOUND } }));
        }

    });
};

Our first test will specifically check that the method behaves correctly when there’s a database error condition. Let’s add the following code after the afterEach method of the test suite:

it('Returns db error', function (done) {

    userModelMock.setError(true);
    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        testUserPassword = 'Password0';

    controller.logon(testUser.email, testUserPassword, function (err, apiResponse) {

        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg).equal(ApiMessages.DB_ERROR);
        done();
    });
});

We put the test within its own “#logon” suite using the describe function. This helps us keep the tests grouped, as we will write more than one test per Controller method.

The it method is the actual test. The first argument is the test’s name, in this case ‘Returns db error’. We gave the test a descriptive name that explains what the test is supposed to do.

In the test, we first force an error condition in the userModelMock variable to simulate a database error. Then, we invoke seedUsers on the mock to create a few test User Model instances, and retrieve one of those instances so we can feed its email property to the AccountController instance under test. We create a dummy value for the password argument before invoking the Controller’s logon method.

We placed the assertions of the test within the callback function that we passed to the logon method. We first assert that the success property of the ApiResponse instance returned by the method is false. This is the expected result – as we forced an error condition on the mock of the User Model, the Controller should signal that the logon operation failed.

The second assertion verifies that the logon method returns a database error in the extras.msg property of the ApiResponse instance. This tells us that the reason the method failed is exactly the reason that we forced on it when we called setError(true) on the UserMock instance, which is equivalent to a database error in the User Model Class.

Ready to test this? Let’s go ahead and run Mocha in the command window that we previously opened. The result should look like the picture below:

tests-account-controller-3

The next behavior that we want to test in the logon method is the fact that upon successful completion, it produces a session variable for the user that attempted the logon operation. Let add the test to the file:

it('Creates user session', function (done) {

    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        testUserPassword = 'Password0';            

    controller.logon(testUser.email, testUserPassword, function (err, apiResponse) {
                
        if (err) return done(err);
        should(apiResponse.success).equal(true);
        should.exist(apiResponse.extras.userProfileModel);
        should.exist(controller.getSession().userProfileModel);
		should(apiResponse.extras.userProfileModel)
.equal(controller.getSession().userProfileModel);
        done();
    });
});

This time we invoked logon, passing the correct email address and password of a known user (the test user). If the logon method is coded correctly, the operation should succeed.

Note how we had to hardcode the password of the test user. We did this because the User Class does not have a password property (we designed it that way for security reasons). We know the value to hardcode from the implementation of the seedUsers method in the UserMock Class.

The first assertion that we put in this test simply verifies that the logon method sets the success property of its ApiResponse instance to true, signaling that the operation succeeded. The second assertion confirms that the logon method returns a UserProfileModel instance in the extras property of its ApiResponse. The third assertion checks that the UserProfileModel instance is also placed in the AccountController’s internal session variable. Finally, we confirm that the UserProfileModel instance returned by the logon method, and the AccountController’s UserProfileModel instance store in the Controller’s internal session variable are the same.

Running Mocha in the command window should show test results similar to these:

tests-account-controller-4

The third behavior we want to verify in the AccountController Class’s logon method is that it returns an “email not found” error when the supplied email address does not exist. We will implement this test with the following code:

it('Returns "Email not found"', function (done) {

    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        testUserPassword = 'Password0',
        nonExistentEmailAddress = 'test';

    controller.logon(nonExistentEmailAddress, testUserPassword, function (err, apiResponse) {

        if (err) return done(err);
        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg)
.equal(ApiMessages.EMAIL_NOT_FOUND);
        done();
    });
});

In this test we created an additional nonExistentEmailAddress variable, that we passed to the logon method. The assertions check that the method returns a failure ApiResponse and that the extras.msg property of the response explains that the email address was not found.

Let’s run the test suite in the command window. The results should look like the picture below:

tests-account-controller-5

In the last test for the logon method we will verify that the method behaves correctly when the supplied password is invalid. Here’s the code for the test:

it('Returns "Invalid password"', function (done) {

    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        invalidPassword = 'Password';

    controller.logon(testUser.email, invalidPassword, function (err, apiResponse) {

        if (err) return done(err);
        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg)
.equal(ApiMessages.INVALID_PWD);
        done();
    });
});

This time we are fed an invalid password to the logon method. The assertions check that the success response is false, and that the extras.msg property of the response explains that call failed because the password is invalid.

Running the suite one more time should produce the results shown below:

tests-account-controller-6

Testing the logoff method

The AccountController Class’s logoff method simply destroys the logged on user’s session variable:

AccountController.prototype.logoff = function () {
    if (this.session.userProfileModel) delete this.session.userProfileModel;
    return;
};

Let’s create a test to check this behavior:

describe('#logoff', function () {
    it('Destroys user session', function (done) {
        controller.logoff();
        should.not.exist(controller.getSession().userProfileModel);
        done();
    });
});

The test is almost as simple as the logoff method itself. After calling logoff, we assert that the AccountController’s userProfileModel session variable does not exist anymore.

Let’s run the test suite in the command window, where we should see the new test passing:

tests-account-controller-7

Testing the register method

The AccountController Class’s register method is a bit more complex. Here’s the code so you can review it before we create the tests for it:

AccountController.prototype.register = function (newUser, callback) {
    var me = this;
    me.userModel.findOne({ email: newUser.email }, function (err, user) {

        if (err) {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
        }

        if (user) {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.EMAIL_ALREADY_EXISTS } }));
        } else {

            newUser.save(function (err, user, numberAffected) {

                if (err) {
                    return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
                }
                    
                if (numberAffected === 1) {

                    var userProfileModel = new me.UserProfileModel({
                        email: user.email,
                        firstName: user.firstName,
                        lastName: user.lastName
                    });

                    return callback(err, new me.ApiResponse({
                        success: true, extras: {
                            userProfileModel: userProfileModel
                        }
                    }));
                } else {
                    return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.COULD_NOT_CREATE_USER } }));
                }             

            });
        }

    });
};

The first test that we will create will check that register method behaves correctly when there’s a database error condition:

describe('#register', function () {

    it('Returns db error', function (done) {

        userModelMock.setError(true);
        userModelMock.seedUsers();
        var testUser = userModelMock.getTestUser();
        controller.register(testUser, function (err, apiResponse) {

            should(apiResponse.success).equal(false);
            should(apiResponse.extras.msg)
.equal(ApiMessages.DB_ERROR);
            done();
        });
    });
});

The key to this test is invoking the UserMock instance’s setError method to set the mock’s internal error variable to true, which is equivalent to causing a database error in the User Model in a real-world scenario. The assertions prove that the method’s success response is false and the extras.msg property explains that the reason for the failure was a database error.

Running the test suite again should produce results similar to the screenshot below:

tests-account-controller-8

The second test for the register method will verify that the register method fails as expected when the email address of the person trying to register already exists:

it('Returns "Email already exists"', function (done) {
                        
    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser();
    controller.register(testUser, function (err, apiResponse) {

        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg)
.equal(ApiMessages.EMAIL_ALREADY_EXISTS);
        done();
    });
});

In this test we are not setting any error conditions explicitly prior to invoking the register method. We are just trying to register a user that already exists in the User Model mock instance, which of course should cause the method to fail. The two assertions check that the method returns an error condition due to an already existing email address.

Back in the command window the test results should show this test passing as well:

tests-account-controller-9

The next test verifies that the register method behaves as expected when the User model cannot be saved in the database:

it('Returns "Could not create user"', function (done) {
            
    var testUser = new UserMock();
    testUser.setNumberAffected(0);

    controller.register(testUser, function (err, apiResponse) {

        if (err) return done(err);
        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg)
.equal(ApiMessages.COULD_NOT_CREATE_USER);
        done();
    });
});

In this test we set the UserMock Class’s internal numberAffected variable to 0 by calling the setNumberAffected method. This simulates the scenario where the Mongoose Model (the User Class) fails to save the new user’s data to the MongoDB database. The two assertions check that the method returns an error condition due to the user creation process failing.

We should see this test passing in the command window as well:

tests-account-controller-10

The final test for the register method will check that the method produces the correct response when the registration succeeds:

it('Registers a user', function (done) {

    var testUser = new UserMock();
    testUser.setNumberAffected(1);

    controller.register(testUser, function (err, apiResponse) {

        if (err) return done(err);
        should(apiResponse.success).equal(true);
        done();
    });
});

In this case we set the UserMock’s numberAffected variable to 1, which means that the User Model instance was successfully serialized and saved to MongoDB. The assertion checks that the success property of the ApiResponse instance generated by the method is set to true.

Let’s run the test suite and confirm that this test is “green”:

tests-account-controller-11

Testing the resetPassword method

The resetPassword method in the AccountAcontroller Class is the method that initiates the two-step password reset procedure that we designed and started implementing in the prior article of this series. Here’s the code for the method:

AccountController.prototype.resetPassword = function (email, callback) {
    var me = this;
    me.userModel.findOne({ email: email }, function (err, user) {

        if (err) {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
        }

        if (user) {
            // Save the user's email and a password reset hash in session.
            var passwordResetHash = me.uuid.v4();
            me.session.passwordResetHash = passwordResetHash;
            me.session.emailWhoRequestedPasswordReset = email;

            me.mailer.sendPasswordResetHash(email, passwordResetHash);

            return callback(err, new me.ApiResponse({ success: true, extras: { passwordResetHash: passwordResetHash } }));
        } else {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.EMAIL_NOT_FOUND } }));
        }        
    })
};

As we’ve done with the prior methods, the first test for resetPassword will verify that it behaves as expected when there’s a database error:

describe('#resetPassword', function () {

    it('Returns db error', function (done) {

        userModelMock.setError(true);
        userModelMock.seedUsers();
        var testUser = userModelMock.getTestUser();
        controller.resetPassword(testUser.email, function (err, apiResponse) {

            should(apiResponse.success).equal(false);
            should(apiResponse.extras.msg)
.equal(ApiMessages.DB_ERROR);
            done();
        });
    });
});

This is familiar territory for us already, so I will not go into the details of the test. Running the suite in the command window should produce the results depicted below:

tests-account-controller-12

The second test for resetPassword will check that the method saves the user’s email, as well as a password reset hash value, in the user’s session:

it('Sets "passwordResetHash" session', function (done) {

    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser();

    controller.resetPassword(testUser.email, function (err, apiResponse) {

        if (err) return done(err);
        should(apiResponse.success).equal(true);
        should.exist(controller.getSession().passwordResetHash);
        should.exist(apiResponse.extras.passwordResetHash);
        should(controller.getSession()
.emailWhoRequestedPasswordReset).equal(testUser.email);
        should(controller.getSession()
.passwordResetHash).equal(apiResponse.extras.passwordResetHash);
        done();
    })
});

The assertions that we placed in this method check that the user’s email and password reset hash are stored in the Controller’s session variable, and that the password reset hash returned by the method is the same as the hash stored in the Controller’s session variable.

Let’s run the suite in the command window and verify that the results look similar to the picture below:

tests-account-controller-13

With the third and last test for the resetPassword method we want to confirm that its behavior is correct when the email address of the person requesting a password reset cannot be found in the database:

it('Returns "Email not found"', function (done) {

    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        nonExistentEmailAddress = 'dummy@email.com';

    controller.resetPassword(nonExistentEmailAddress, function (err, apiResponse) {

        if (err) return done(err);
        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg)
.equal(ApiMessages.EMAIL_NOT_FOUND);
        done();
    })
});

After feeding the method a non-existent email address, the assertions check that the ApiResponse returned reports that the reason for the failure is that the email address was not found.

Back in the command window the test suite should show this test passing:

tests-account-controller-14

Testing the resetPasswordFinal method

The last test suite that we will create for the AccountController Class will target the resetPasswordFinal method. This is the method that’s invoked when users perform the second step of the password reset procedure, which requires them to enter the password reset hash that the application sent to them via email. Here’s the method’s code for your reference:

AccountController.prototype.resetPasswordFinal = function (email, newPassword, passwordResetHash, callback) {
    var me = this;
    if (!me.session || !me.session.passwordResetHash) {
        return callback(null, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.PASSWORD_RESET_EXPIRED } }));
    }

    if (me.session.passwordResetHash !== passwordResetHash) {
        return callback(null, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.PASSWORD_RESET_HASH_MISMATCH } }));
    }

    if (me.session.emailWhoRequestedPasswordReset !== email) {
        return callback(null, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.PASSWORD_RESET_EMAIL_MISMATCH } }));
    }

    var passwordSalt = this.uuid.v4();

    me.hashPassword(newPassword, passwordSalt, function (err, passwordHash) {

        me.userModel.update({ email: email }, { passwordHash: passwordHash, passwordSalt: passwordSalt }, function (err, numberAffected, raw) {

            if (err) {
                return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
            }

            if (numberAffected < 1) {

                return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.COULD_NOT_RESET_PASSWORD } }));
            } else {
                return callback(err, new me.ApiResponse({ success: true, extras: null }));
            }                
        });
    });
};

We designed the password reset procedure so that it is active during a limited time period once it has started. As a security measure, we do not want the password reset links to remain active for long periods of time. The first test for resetPasswordFinal will check the method’s behavior when the password reset period has expired:

describe('#resetPasswordFinal', function () {

    it('Returns "Password reset expired"', function (done) {

        var email = 'irrelevant to the test',
            newPassword = 'irrelevant to the test';
            passwordResetHash = 'irrelevant to the test';
        controller.setSession({}); // Destroy session.passwordResetHash.
        controller.resetPasswordFinal(email, newPassword, passwordResetHash, function (err, apiResponse) {
            should(apiResponse.success).equal(false);
            should(apiResponse.extras.msg)
.equal(ApiMessages.PASSWORD_RESET_EXPIRED);
            done();
        });
    });
        
});

In the test, prior to invoking resetPasswordFinal, we set the Controller’s internal session variable to a new “empty” object. With this step we simulate expiring an existing session and therefore ending the time period during which a started password reset procedure is active. The assertions we placed in this test should confirm that the password reset procedure cannot continue because the active period has expired.

Over in the command window we should be able to see the test passing:

tests-account-controller-15

A situation that can occur with the password reset procedure is that someone with questionable intentions will access the password reset link and provided a password reset hash generated by brute force methods or other means. In such a case, the resetPasswordFinal method is expected to reject the provided hash and stop the password reset procedure.

We will test the method’s behavior using the following code:

it('Returns "Invalid password reset hash"', function (done) {
            
    var email = 'irrelevant to the test',
        newPassword = 'irrelevant to the test',
        passwordResetHash = uuid.v4();
    controller.setSession({ passwordResetHash: uuid.v4() });
    controller.resetPasswordFinal(email, newPassword, passwordResetHash, function (err, apiResponse) {
        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg)
.equal(ApiMessages.PASSWORD_RESET_HASH_MISMATCH);
        done();
    });
});

In the test create an arbitrary password reset hash to be fed to the resetPasswordFinal method, and we set the password reset hash stored in the Controller’s session variable to a different arbitrary value. This mimics someone entering a hash that’s different from the one stored in the Controller.

The assertions in the test check that the resetPasswordFinal returns with failure, and explains that the reason for the failure is that the hashes are different.

Here’s how the command window’s output should look:

tests-account-controller-16

Another situation that can occur with the password reset procedure is that someone will access the password reset link and provide an email address that is no longer valid in the application. If this happens, we expect the resetPasswordFinal method to fail and reject the provided email address:

it('Returns "Invalid password reset email"', function (done) {

    var email = 'test value',
        newPassword = 'irrelevant to the test',
        emailWhoRequestedPasswordReset = 'a different test value',
        passwordResetHash = 'irrelevant to the test';
    controller.setSession({ passwordResetHash: passwordResetHash, emailWhoRequestedPasswordReset: emailWhoRequestedPasswordReset });    controller.resetPasswordFinal(email, newPassword, passwordResetHash, function (err, apiResponse) {
        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg)
.equal(ApiMessages.PASSWORD_RESET_EMAIL_MISMATCH);
        done();
    });
});

In the test we feed the resetPasswordFinal method an email address value that is different from the one we store in the Controller’s session variable. We also make sure that the password reset hash stored in the session variable and the hash passed to the method are the same, otherwise the method is going to fail due to hashes mismatch.

The assertions verify that under these conditions the method returns an ApiResponse instance whose extras.msg property explains that the email address supplied is not valid.

Executing the test suite in the command window should show the test passing:

tests-account-controller-17

The following test will verify that the method behaves as expected when there’s a database error:

it('Returns db error', function (done) {

    userModelMock.setError(true);
    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        passwordResetHash = uuid.v4(),
        newPassword = 'NewPassword';

    controller.setSession({ passwordResetHash: passwordResetHash, emailWhoRequestedPasswordReset: testUser.email });

    controller.resetPasswordFinal(testUser.email, newPassword, passwordResetHash, function (err, apiResponse) {

        should(apiResponse.success).equal(false);
        should(apiResponse.extras.msg).equal(ApiMessages.DB_ERROR);
        done();
    });
});

Here we set the UserMock’s error variable to true to simulate a database error. The assertion checks that the method fails and returns a database error message.

The results of executing the test suite should show this test passing as well:

tests-account-controller-18

Another behavior that we want to test in the resetPasswordFinal method is what happens when the Mongoose Model returns that no records were affected by the update in the MongoDB database:

it('Returns "Could not reset password"', function (done) {

    userModelMock.setNumberAffected(0);
    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        passwordResetHash = uuid.v4(),
        newPassword = 'NewPassword';

    controller.setSession({ passwordResetHash: passwordResetHash, emailWhoRequestedPasswordReset: testUser.email });

    controller.resetPasswordFinal(testUser.email, newPassword, passwordResetHash, function (err, apiResponse) {

        should(apiResponse.success).equal(false);        should(apiResponse.extras.msg)
.equal(ApiMessages.COULD_NOT_RESET_PASSWORD);
        done();
    });
});

In this case we set the numberAffected property of the UserMock Class to 0, which simulates the Mongoose Model returning no records affected by the update in the MongoDB database. The assertions check that the ApiResponse returned by the method corresponds to the error condition that we are testing.

Let’s run the test suite in the command window one more time. All the tests should pass as expected:

tests-account-controller-19

And finally, we are going to test that the method succeeds when there are no error conditions present:

it('Resets user\'s password', function (done) {

    userModelMock.setNumberAffected(1);
    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
        passwordResetHash = uuid.v4(),
        newPassword = 'NewPassword';

    controller.setSession({ passwordResetHash: passwordResetHash, emailWhoRequestedPasswordReset: testUser.email });

    controller.resetPasswordFinal(testUser.email, newPassword, passwordResetHash, function (err, apiResponse) {

        if (err) return done(err);
        should(apiResponse.success).equal(true);
        done();
    })
});

In the test we set the UserMock instance to a valid state, as well as the AccountController instance’s session variable. The assertion checks that the method returns a success response.

One last run of the test suite in the command window should show the last test passing:

tests-account-controller-20

Summary and Next Steps

In this Mocha and Node JS article we created unit tests for the User Account Controller of the Node.js and MongoDB backend of the meeting room booking mobile application that we started building in the first article of this series.

The tests that we created target the features of the Account Controller’s public interface. They give us a high level of confidence that if we ever need to refactor the Controller, we will have a safety net that will prevent us from breaking the Controller’s public interface, which other modules of the applications depend on.

In the next article of this tutorial we will implement the HTTP endpoint for the User Account Controller. This endpoint will make the user registration, logon, logoff and password reset features of the Account Controller available via HTTP requests. The meeting room booking mobile app that we are building will make requests to this endpoint.

Remember to sign up for MiamiCoder’s newsletter so you can be among the first to know when the next part of this tutorial is available. I will also send all my free tutorials and tools directly in your inbox.

Download Source Code

Download the source code for this article: Testing a Node.js backend with Mocha on GitHub

All the Chapters of this Series

You can find all the published parts of this series here: The Meeting Room Booking App Tutorial.

Stay Tuned

Don’t miss out on the updates! Make sure to sign up for my newsletter so you can get MiamiCoder’s new articles and updates sent free to your inbox.

Tagged With: NodeJS Tutorial 11 Comments

Comments

  1. Abdu says

    January 2, 2015 at 9:04 AM

    There is an error while testing the above example.

    The error log says:
    \walk3_2\server>mocha

    1) “before each” hook
    2) “after each” hook

    0 passing (6ms)
    2 failing

    1) “before each” hook:
    TypeError: object is not a function
    at Context.testUserPassword (D:\XDK Intel Fast Track\fast-track-to-intel-x
    dk-new-walk3_2\walk3_2\server\test\account-controller-test.js:23:21)
    at Hook.Runnable.run (C:\Users\Dell1\AppData\Roaming\npm\node_modules\moch
    a\lib\runnable.js:218:15)
    at next (C:\Users\Dell1\AppData\Roaming\npm\node_modules\mocha\lib\runner.
    js:259:10)
    at Object._onImmediate (C:\Users\Dell1\AppData\Roaming\npm\node_modules\mo
    cha\lib\runner.js:276:5)
    at processImmediate [as _immediateCallback] (timers.js:354:15)

    2) “after each” hook:
    TypeError: Cannot call method ‘setError’ of undefined
    at Context. (D:\XDK Intel Fast Track\fast-track-to-intel-xdk-ne
    w-walk3_2\walk3_2\server\test\account-controller-test.js:30:19)
    at Hook.Runnable.run (C:\Users\Dell1\AppData\Roaming\npm\node_modules\moch
    a\lib\runnable.js:218:15)
    at next (C:\Users\Dell1\AppData\Roaming\npm\node_modules\mocha\lib\runner.
    js:259:10)
    at Object._onImmediate (C:\Users\Dell1\AppData\Roaming\npm\node_modules\mo
    cha\lib\runner.js:276:5)
    at processImmediate [as _immediateCallback] (timers.js:354:15)

    Reply
    • Jorge says

      January 2, 2015 at 9:36 AM

      Make sure that all your references are installed correctly, in particular the UserMock. Check this line: UserMock = require(‘./user-mock.js’).

      Reply
  2. nuoc hoa that says

    April 14, 2015 at 2:18 AM

    Great post.

    Reply
  3. Matt says

    April 29, 2015 at 5:51 AM

    Hi Jorge,

    I cannot work out why I keep getting this error, I have checked many times and user.js is pointing to the correct place,

    matt:server gradproject$ mocha
    js-bson: Failed to load c++ bson extension, using pure JS version
    module.js:338
    throw err;
    ^
    Error: Cannot find module ‘../models/user.js’
    at Function.Module._resolveFilename (module.js:336:15)
    at Function.Module._load (module.js:278:25)
    at Module.require (module.js:365:17)
    at require (module.js:384:17)
    at Object. (/Users/gradproject/Desktop/user_login/app/server/test/account-controller-test.js:6:12)
    at Module._compile (module.js:460:26)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Module.require (module.js:365:17)
    at require (module.js:384:17)
    at /usr/local/lib/node_modules/mocha/lib/mocha.js:192:27
    at Array.forEach (native)
    at Mocha.loadFiles (/usr/local/lib/node_modules/mocha/lib/mocha.js:189:14)
    at Mocha.run (/usr/local/lib/node_modules/mocha/lib/mocha.js:422:31)
    at Object. (/usr/local/lib/node_modules/mocha/bin/_mocha:398:16)
    at Module._compile (module.js:460:26)
    at Object.Module._extensions..js (module.js:478:10)
    at Module.load (module.js:355:32)
    at Function.Module._load (module.js:310:12)
    at Function.Module.runMain (module.js:501:10)
    at startup (node.js:129:16)
    at node.js:814:3

    Could it be that I haven’t installed node.js, express, mongodb, and mongoose in the right place? Any help would be really appreciated, thanks 🙂

    Matt.

    Reply
    • Jorge says

      April 29, 2015 at 7:11 AM

      Yes, please make sure that everything is installed correctly and re-check all your file references. Sometimes it’s a small detail that you overlooked.

      Reply
      • Matt says

        April 29, 2015 at 10:13 AM

        Thanks again Jorge for the quick reply,

        Do you happen to know any good tutorials that go through how to download all the relevant elements and which directories I should be installing them into?

        Thanks 🙂

        Reply
  4. jason says

    May 17, 2015 at 10:25 PM

    your reset password function is different on the previous article than in this article.

    Reply
  5. kalyan chakravarthy says

    October 1, 2016 at 5:18 AM

    Hi

    Thanks for the great tutorial. can you help me with the error
    assertionError expected true to be false for this code

    it(‘Returns “Email not found”‘, function (done) {

    userModelMock.seedUsers();
    var testUser = userModelMock.getTestUser(),
    nonExistentEmailAddress = ‘dummy@email.com’;

    controller.resetPassword(nonExistentEmailAddress, function (err, apiResponse) {

    if (err) return done(err);
    should(apiResponse.success).equal(false);
    should(apiResponse.extras.msg)
    .equal(ApiMessages.EMAIL_NOT_FOUND);
    done();
    })
    });

    Reply
  6. Ben says

    November 8, 2016 at 6:54 AM

    Hi Jorge,

    I’m receiving the error: ‘DeprecationWarning: crypto.pbkdf2 without specifying a digest is deprecated. Please specify a digest’ in the cmd whenever I try to run the program. Any ideas what could be causing that? If you need any more info let me know! 🙂

    Cheers,
    Ben

    Reply
    • Jorge says

      November 14, 2016 at 4:34 PM

      Looks like the method I used is deprecated. Please look up the new method signature in the crypto docs.

      Reply
  7. Arad says

    June 30, 2017 at 2:38 AM

    Hello,
    Thank you for this informative article. Unfortunately, when I try to run the mocha command I get this message:

    Cannot find module ‘mongodb/node_modules/bson’

    and I could not continue.
    I have downloaded the source code and installed the dependencies.However, I still got this message!
    Would you please help me.
    Thanks in advance

    Reply

Leave a Comment Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Get My Books

The beginner's guide to Sencha Touch apps
The beginner's guide to jQuery Mobile apps

Book: How to Build a jQuery Mobile Application

Topics

  • » jQuery Mobile Tutorials
  • » Sencha Touch Tutorials
  • » ExtJS Tutorials
  • » Books
  • » Tools
  • » .Net Tutorials
  • » BlackBerry Tutorials
  • » iOS Tutorials
  • » Node.js Tutorials
  • » Android Training
  • » BlackBerry Training
  • » iOS Training

Search

Contact Me

  •  Email: jorge[AT]jorgeramon.me
  •  Twitter: @MiamiCoder
  •  LinkedIn: Jorge Ramon


Don’t Miss the Free Updates

Receive free updates in your inbox.
Your address is safe with me.

Copyright © 2021 Jorge Ramon · The opinions expressed herein do not represent the views of my employers in any way · Log in