Chapter 11. Unit Testing

Unit testing has become a primary part of good software development practice. It is a method by which individual units of source code are tested to ensure proper functioning. Each unit is theoretically the smallest testable part of an application. In a Node.js application, you might consider each module as a unit.

In unit testing, each unit is tested separately, isolating the unit under test as much as possible from other parts of the application. If a test fails, you would want it to be due to a bug in your code rather than a bug in the package that your code happens to use. Common technologies to use are mock objects and other methods to present a faked dependency implementation. We will focus on that testing model in this chapter.

Functional testing, on the other hand, doesn't try to test individual components, but instead it tests the whole system. Generally speaking, unit testing is performed by the development team, and functional testing is performed by a QA or Quality Engineering (QE) team. Both testing models are needed to fully certify an application. An analogy might be that unit testing is similar to ensuring that each word in a sentence is correctly spelled, while functional testing ensures that the paragraph containing that sentence has a good structure. We'll also do a little bit of functional testing.

Now that we've written several chunks of software in this book, let's use that software to explore unit test implementation.

Testing asynchronous code

The asynchronous nature of the Node.js platform means accounting for asynchronous behavior in tests. Fortunately, the Node.js unit test frameworks help in dealing with this, but it's worth spending a few moments considering the underlying problem.

Consider a code snippet like this, which you could save in a file named deleteFile.js:

const fs = require('fs');
exports.deleteFile = function(fname, callback) {
 fs.stat(fname, (err, stats) => {
 if (err)
  callback(new Error(`the file ${fname} does not exist`));
 else {
  fs.unlink(fname, err2 => {
   if (err)
   callback(new Error(`could not delete ${fname}`));
   else callback();
  });
 }
 });
};

The nature of asynchronous code is such that its execution order is nonlinear, meaning that there is a complex relationship between time and the lines of code. Since this is the real world, and not science fiction, we don't have a time machine (blue box or not) to help us traverse the web of time, and therefore, we must rely on software tools. The code is not executed one line after another, but each callback is called according to the flow of events. This snippet, like many written with Node.js, has a couple of layers of callback, making the result arrive in an indeterminate amount of time in the future.

Testing the deleteFile function can be accomplished with this code which you can save in test-deleteFile.js:

const df = require('./deleteFile');
df.deleteFile("no-such-file", err => {
    // the test passes if err is an Error with the message
    //     "the file no-such-file does not exist"
    // otherwise the test fails
});

This is what we call a negative test. It verifies that the failure path actually executes. It's relatively easy to verify which path was taken within the deleteFile function, by inspecting the err variable. But, what if the callback is never called? That would also be a failure of the deleteFile function, but how do you detect that condition? You might be asking, how could it be that the callback is never called? The behavior of file access over NFS includes conditions where the NFS server is wedged and filesystem requests never finish, in which case the callbacks shown here would never be called.

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

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