13. Testing

With all the code you have written so far, you’re now ready to look at how to test it to make sure it works. There are many models and paradigms through which this is done, and Node.js supports most of them. This chapter looks at some of the more common ones and then looks at how you can do functional testing of not only synchronous APIs but also Node’s asynchronous code and then add some testing to your photo album application.

Choosing a Framework

A few common models of testing are available, including test-driven development (TDD) and behavior-driven development (BDD). The former focuses highly on making sure that all your code is properly exposed to testing (indeed, in many cases, demanding that you write no code until you’ve written the tests for it), whereas the latter focuses more on the business requirements of a particular unit or module of code and encourages testing to be a bit more holistic than simple unit testing.

Regardless of the model to which you subscribe (or will subscribe, if this is your first time writing tests), adding tests to your code base is always a good idea, because you want to make sure not only that what you have written works but also that future changes you make to your code base don’t break things. Adding testing to your Node applications adds a few challenges because you frequently need to mix synchronous, asynchronous, and RESTful server API functionality together, but the Node.js platform is sufficiently robust and advanced already that there are options for all your needs.

Of the many frameworks available today, two stand out in popularity:

Image nodeunit—This is a simple and straightforward test runner for testing both in Node and in the browser. It’s extremely easy to work with and gives you a lot of flexibility for setting up your testing framework.

Image Mocha—Based on an older testing framework called Expresso, Mocha is basically a more feature-rich framework for Node that focuses on being easier to use and more enjoyable to code. It has some cool features for asynchronous testing and an API for output formatting.

Although all they work perfectly fine and have their places, for the first part of this chapter you can stick with the reasonably straightforward nodeunit, largely because it’s really easy to work with and demonstrate. You’ll then take a quick look at some black-box API testing using Mocha.

Installing Nodeunit

For your projects now, you can create a test/ subfolder into which you put all the files, samples, and data files related to the testing of your various projects. The first file to put in is package.json, as follows:

{
  "name": "API-testing-demo",
  "description": "Demonstrates API Testing with nodeunit",
  "version": "0.0.2",
  "private": true,
  "dependencies": {
    "nodeunit": "0.x"
  }
}

When you run npm update, nodeunit is installed, and you can begin writing and running tests.

Writing Tests

The nodeunit framework organizes tests into modules, with each exposed function acting as a test and each exposed object acting as a group of tests. To each test, you are given an object that will help you perform the tests and tell nodeunit when you are done:

exports.test1 = function (test) {
    test.equals(true, true);
    test.done();
}

You must call test.done at the end of every single test; otherwise, nodeunit does not know when it is finished. To run this test, save it to a file called trivial.js and then run the provided running script in the node_modules/.bin folder. On both UNIX/Mac platforms and Windows, you run (switch the / characters for characters on Windows, of course)

node_modules/.bin/nodeunit trivial.js

You see something similar to

C:UsersMarka> node_modules.bin odeunit.cmd trivial.js

trivial.js
(Image)test1

OK: 1 assertions (0ms)

C:UsersMarka>

Simple Functional Tests

For each test you write, you want to do three things:

1. Call the expect method on the provided test parameter with the number of conditions nodeunit should expect you to verify in this particular test. This step is optional, but completing it is a good idea in case some of the code accidentally skips over a test.

2. For each condition you want to verify, you should call some sort of assertion function (see Table 12.1) to verify you’re seeing what you expect to see. The first example you’ve seen of this is test.equals.

3. Call test.done at the end of every single test to tell nodeunit you are finished.

Now take the code you wrote for a reverse Polish notation calculator in the preceding chapter and put it in a file called rpn.js (see Listing 13.1).

Listing 13.1 The rpn.js file


// push numbers onto a stack, pop when we see an operator.
exports.version = "1.0.0";

exports.compute = function (parts) {
    var stack = [];
    for (var i = 0; i < parts.length; i++) {
        switch (parts[i]) {
          case '+': case '-': case '*': case '/':
            if (stack.length < 2) return false;
            do_op(stack, parts[i]);
            break;
        default:
            var num = parseFloat(parts[i]);
            if (isNaN(num)) return false;
            stack.push(num);
            break;
        }
    }
    if (stack.length != 1) return false;
    return stack.pop();
}

function do_op(stack, operator) {
    var b = stack.pop();
    var a = stack.pop();
    switch (operator) {
      case '+': stack.push(a + b); break;
      case '-': stack.push(a - b); break;
      case '*': stack.push(a * b); break;
      case '/': stack.push(a / b); break;
      default:  throw new Error("Unexpected operator");
    }
}


Now, write some tests for it (don’t forget to use require in the rpn.js file in your test file):

exports.addition = function (test) {
    test.expect(4);
    test.equals(rpn.compute(prep("1 2 +")), 3);
    test.equals(rpn.compute(prep("1 2 3 + +")), 6);
    test.equals(rpn.compute(prep("1 2 + 5 6 + +")), 14);
    test.equals(rpn.compute(prep("1 2 3 4 5 6 7 + + + + + +")), 28);
    test.done();
};

The prep function just splits the provided string into an array:

function prep(str) {
    return str.trim().split(/[ ]+/);
}

You can repeat this for all the operators your calculator supports (subtraction, multiplication, and division), and even write some additional ones for decimal numbers:

exports.decimals = function (test) {
    test.expect(2);
    test.equals(rpn.compute(prep("3.14159 5 *")), 15.70795);
    test.equals(rpn.compute(prep("100 3 /")), 33.333333333333336);
    test.done();
}

So far you’ve only used the test.equals assertion to verify that values are what you expect. Nodeunit uses a module from npm called assert, however, and there are many other possibilities, as shown in Table 13.1.

Image

Table 13.1 Testing Assertions

So, you can now add a new test to make sure that the calculator rejects empty expressions:

exports.empty = function (test) {
    test.expect(1);
    test.throws(rpn.compute([]));
    test.done();
};

When the tests fail, nodeunit tells you loudly, giving you both the condition that failed and a full stack trace of calls that led up to that failure:

01_functional.js
Image addition

AssertionError: 28 == 27
    at Object.assertWrapper [as equals] [...]tests/node_modules/nodeunit/lib/types.
js:83:39)
    at Object.exports.addition [...]tests/01_functional.js:9:10)
   (etc)
(Image) subtraction
(Image) multiplication
(Image) division
(Image) decimals
(Image) empty

FAILURES: 1/17 assertions failed (5ms)

You can then look in your code to see which test is causing the problem and analyze the problem or regression.

Groups of Tests

You can add groups of tests, as follows, which also allow you to add setUp and tearDown methods, which will be called before and after (respectively) all the tests in the group, as follows:

exports.group1 = {
    setUp: function (callback) {
      callback();
    },
    tearDown: function (callback) {
      callback();
    },
    test1: function (test) {
      test.done();
    },
    test2: function (test) {
      test.done();
    },
    test3: function (test) {
      test.done();
    }
};

In the above group, the order in which the functions would be called is:

  setUp → test1 → test2 → test3 → tearDown

Note that for the setUp and tearDown functions you have to call callback in order to continue.

Testing Asynchronous Functionality

Because you do so much asynchronous programming in Node.js, many of the tests are also asynchronous. Nodeunit is designed with this in mind: your tests can take as long as they want to run and be as asynchronous as they want, as long as they call test.done when they’re finished executing. As an example, write a couple of asynchronous tests right now:

exports.async1 = function (test) {
    setTimeout(function () {
        test.equal(false, true);
        test.done();
    }, 2000);
};

exports.async2 = function (test) {
    setTimeout(function () {
        test.equal(true, true);
        test.done();
    }, 1400);
};

Running this test module provides the following output (note that the runtime is roughly the combination of the two tests run serially):

hostnameKimidori: functional_tests marcw$ node_modules/.bin/nodeunit 02_async.js

02_async.js
(Image) async1
(Image) async2

OK: 2 assertions (3406ms)

API Testing

Functional unit testing is very important to make sure that your code and functions work properly, but you also want to test how your APIs work as a “black box” from the outside. Does your API do the right thing given various inputs? Does it return the correct errors when you try to do things you should not be allowed to do?

To write tests for this, you are going to look at the Mocha testing framework, along with an important assertion framework called Chai that integrates seamlessly into Mocha.

Before you start, you’ll want to install Mocha globally so you can use the mocha command to run your tests. On Mac or Linux machines, you’ll need to run:

sudo npm install -g mocha

whereas on Windows machines, you’ll want to run a cmd.exe with administrator permissions and just run:

npm install -g mocha

When this is done, go to the folder of your preferred version of the photo-sharing application, either the MongoDB one from Chapter 8 or the MySQL one from Chapter 9. It doesn’t actually matter which one you run; you’re doing black box testing, so what’s running internally within the application is less important!

Clear out the database (either by deleting the data/ folder for MongoDB or by rerunning schema.sql with the mysql command line utility), and then run the server:

node server.js

Each time before you rerun your tests, you’ll want to clear out the database as above so you can be sure to run the tests in an artifact-free environment.

Now you can start to edit a file called chaitest.js. Put the following code in it:

var chai = require("chai"),
    chaihttp = require("chai-http"),
    mocha = require("mocha");

var agent = chai.request.agent("http://localhost:8080");
var expect = chai.expect;
var sid;

chai.use(chaihttp);

describe("Server Testing", function () {
        it("Should get nothing from /v1/albums");
        it("Should fail to add an album");
        it("Should register a new user");
        it("Should login a user");
});

All the code before the describe function sets up your testing environment—you include Mocha and Chai, along with the Chai HTTP framework that will allow you to make requests to your server. You set the server and remember the agent so you can re-use this in your tests.

Finally, you describe your tests. You’ve created a group called “Server Testing” and then added four tests to it that do the specified things. Run this with:

mocha chaitest.js

You should see:

  Server Testing
    - Should get nothing from /v1/albums
    - Should fail to add an album
    - Should register a new user
    - Should login a user


  0 passing (6ms)
  4 pending

None of your tests are passing because they don’t do anything yet. Let’s fill in the first to get /v1/albums.json and verify you get nothing back:

  it("Should get nothing from /v1/albums", function (done) {
    agent
      .get("/v1/albums.json")
      .end(function (err, res) {
        sid = res.headers['set-cookie'].pop().split(";")[0];
        expect(res).to.have.status(200);
        expect(res).to.be.json;
        expect(res.body.error).to.be.null;
        done();
      });
  });

This is your first introduction to how Chai works in Node.js—it almost feels like you’re writing regular English sentences! You’ll see this as you work your way through all the Chai examples in this book and anywhere else you search on the internet: they always feel very natural to write.

You save out the session-id that you get back from your photo albums server here because you need it to do requests after you’re logged in, then you check a few things about the response (that you got 200 back, that it’s JSON, and that it’s empty).

Finally, you call the done function, which is how Mocha knows that you’re done with the test, and it can evaluate everything to see how you did.

For the next test, you’ll want to be sure that you get 403 back from the apps when you try to add an album without being authenticated:

  it("Should fail to add an album", function (done) {
    agent
      .put("/v1/albums.json")
      .send({ name: "testing2012", date: "2012-12-1",
              title: "Testing album", description: "So awesome"})
      .end(function (err, res) {
        expect(res).to.have.status(403);
        done();
      });
  });

Here you use the send method to add POST data to the request. When you get back 403, you can be sure that unauthenticated people cannot add albums, which is the desired behavior.

Next, you create a user and login:

  it ("Should register a new user", function (done) {
    agent
      .put("/v1/users.json")
      .set("Cookie", sid)
      .send({ display_name: "marcwan",
              email_address: "[email protected]",
              password: "abc123" })
      .end(function (err, res) {
        expect(res).to.have.status(200);
        expect(res.body).to.be.object;
        expect(res.body.error).to.be.null;
        expect(res.body.data).to.be.object;
        var user = res.body.data.user;
        expect(user.display_name).to.be.equal("marcwan");
        expect(user.email_address).to.be.equal("[email protected]");
        expect(user.password).to.be.undefined;
        done();
      });
  });

  it("Should login a user", function (done) {
    agent
      .post("/service/logintest")
      .set("Cookie", sid)
      .send({ username: "marcwan", password: "abc123" })
      .end(function (err, res) {
        expect(res).to.have.status(200);
        done();
      });
  });

Not surprisingly, you PUT the new user request, verify that the results are valid, and then login afterwards using the POST. Note that you are now passing the session-id (which you store in the global variable sid) into each request via the set method with the Cookie.

There is an interesting thing above, and that is that you do not call /service/login to log in to your server in the second test above. This is because /service/login redirects to /pages/home/login after a successful login. But this is not what you want for your tests. You just want to know that your login has succeeded, so this is one case where it will actually make sense for you to create a new API just for testing that basically does the same thing as the regular login API but without the redirect. So you created the /service/logintest API to help with this. It is pretty common to see the addition of testing-only versions of code when testing large systems.

Finally, now that you’re logged in, you can verify that you can add an album, as follows:

  it("should add an album", function (done) {
    agent
    .put("/v1/albums.json")
    .set("Cookie", sid)
    .send({ name: "testing2012", date: "2012-12-1",
            title: "Testing album", description: "So awesome"})
    .end(function (err, res) {
      expect(res).to.have.status(200);
      done();
    });
  });

Now that you’re logged in, the add will work.

Summary

Testing your Node.js apps and scripts is both simple and fast with many of the available testing frameworks available via npm. In this chapter, I showed you not only how to use nodeunit, a popular TDD framework, to test the synchronous and asynchronous portions of your applications but also how to use Mocha and Chai to perform full API-level testing of your JSON servers.

With this knowledge tucked under your belt, you’ve come to the end of your tour of Node.js. I hope that I’ve been able to convey some of the reasons why Node.js is so unbelievably exciting and fun to develop with and encourage you to type things and play with the platform as you’ve been reading along.

Sit down and start to write some code. If you have an idea for a website, start putting together some pages and APIs for it. If you do mobile development, think about the ways in which you can deliver a server to mobile customers and figure out how to use Node to do that. Even if you’re just looking to script some things on a server, play around and start writing code today. The only way to get better at programming on any platform is to just use it.

If you run into problems, don’t forget that the Node.js community is extremely active and helpful. With so many helpful and motivated members, it’s certain you’ll be able to meet many like-minded users and get all the resources and help you need to build interesting, useful, and fun applications.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset