By Mike Cantelon, et al.

In this article, you’ll learn about some functional testing solutions for Node, and about setting up test environments based on your own requirements.

Save 37% off Node.js in Action, Second Edition with code fcccantelon at manning.com.

This article was originally posted at: https://www.sitepoint.com/testing-node-applications/

Functional testing

In most web development projects, functional tests work by driving the browser, then checking for various DOM transformations against a list of user-specific requirements. Imagine you’re building a content management system. A functional test for the image library upload feature uploads an image, checks that it gets added, and then checks that it’s added to a corresponding list of images.

The choice of tools to implement functional testing in Node is bewildering. From a high level they fall into two broad groups: headless and browser-based tests. Headless tests typically use something like PhantomJS to provide a terminal-friendly browser environment, but lighter solutions use libraries such as Cheerio and JSDOM. Browser-based tests use a browser automation tool such as Selenium (http://www.seleniumhq.org/) that allows you to write scripts that drive a real browser. Both approaches can use the same underlying Node test tools, and you can use Mocha, Jasmine, or even Cucumber to drive Selenium against your application. Figure 1 shows an example test environment.


Figure 1 Testing with browser automation


Selenium

Selenium is a popular Java-based browser automation library. With the aid of a language-specific driver, you can connect to a Selenium server and run tests against a real browser. In this article, you’ll learn how to use WebdriverIO (http://webdriver.io/), a Node Selenium driver.

Getting Selenium running is trickier than pure Node test libraries, because you need to install Java and download the Selenium JAR file. Download Java for your operating system, and then go to the Selenium download site (http://docs.seleniumhq.org/download/) to download the JAR file. You can then run a Selenium server like this:

 

 java -jar selenium-server-standalone-2.53.0.jar
 

Note that your exact Selenium version may be different. You may also have to supply a path to the browser binary. For example, in Windows 10 with Firefox set as the browserName, you can specify Firefox’s full path like this:

 java -jar -Dwebdriver.firefox.driver="C:\path\to\firefox.exe" selenium-server-standalone-3.0.1.jar

The exact path depends on how Firefox is installed on your machine. For more about the Firefox driver, read the SeleniumHQ documentation (https://github.com/SeleniumHQ/selenium/wiki/FirefoxDriver). You can find drivers for Chrome and Microsoft Edge that are configured in similar ways.

Now create a new Node project and install WebdriverIO:

 

 mkdir -p selenium/test/specs
 cd selenium
 npm init -y
 npm install --save-dev webdriverio
 npm install --save express
 

WebdriverIO comes with a friendly config file generator. To run it, run wdio config:

 

 ./node_modules/.bin/wdio config
 

Follow the questions and accept the defaults. Figure 2 shows my session.


Figure 2 Using wdio to configure Selenium tests


Update the package.json file with the wdio command to allow tests to be run with npm test:

 

  "scripts": {
    "test": "wdio wdio.conf.js"
  },
 

Now add something to the test. A basic Express server will suffice. The example is used in the subsequent listing for testing. Save this listing as index.js.

Listing 1 Sample Express project

 

 const express = require('express');
 const app = express();
 const port = process.env.PORT || 4000;
 
 app.get('/', (req, res) => {
  res.send(`
 <html>
  <head>
    <title>My to-do list</title>
  </head>
  <body>
    <h1>Welcome to my awesome to-do list</h1>
  </body>
 </html>
  `);
 });
 
 app.listen(port, () => {
  console.log('Running on port', port);
 });
 

The good thing about WebdriverIO is that it provides a simple, fluent API for writing Selenium tests. The syntax is clear and easy to learn—you can even write tests with CSS selectors. The next listing (found in test/specs/todo-test.js in the book’s sample code) shows a simple test that sets up a WebdriverIO client and then checks the title on the page.

Listing 2 A WebdriverIO test

 

 const assert = require('assert');
 const webdriverio = require('webdriverio');
 
 describe('todo tests', () => {
  let client;
 
  before(() => {
    client = webdriverio.remote();                              #1
    return client.init();
  });
 
  it('todo list test', () => {
    return client
      .url('/')                                                 #2
      .getTitle()                                               #A
        .then(title => assert.equal(title, 'My to-do list'));   #B
  });
 });
 

#1 Set up WebdriverIO client

#2 Get home page

#A Get title from head

#B Assert title is expected


After WebdriverIO is connected (#1), you can use an instance of the client to fetch pages from your app (#2). Then you can query the current state of the document in the browser—this example uses getTitle to get the title element from the document’s head. If you want to query the document for CSS elements, you can use .elements instead (http://webdriver.io/api/protocol/elements.html). Several kinds of methods for manipulating the document, forms, and even cookies exist.

This test can run a real browser against a Node web app. To run it, start the server on port 4000:

 

 PORT=4000 node index.js
 

Then type npm test. You should see Firefox open and the tests run in the command-line. If you want to use Chrome, open wdio.conf.js and change the browserName property.

 

More-advanced testing with Selenium

 

Dealing with failing tests

When you’re working on an established project, there’ll come a point when tests begin to fail. Node provides several tools for getting more detail on failed tests. Let’s talk about how to enrich the output generated when debugging failing tests.

The first thing to do when tests fail is to generate more-verbose logging output. The next section demonstrates how to do that with NODE_DEBUG.

Getting more-detailed logs

When tests fail, it’s useful to get information on what the program is doing. Node has two ways to do this: one for Node’s internals, and another for npm modules. To debug Node’s core modules, use NODE_DEBUG.

Using NODE_DEBUG

To see how NODE_DEBUG works, imagine you’ve a deeply nested filesystem call where you’ve forgotten to use a callback. For example, the following example throws an exception:

 

 const fs = require('fs');
 
 function deeplyNested() {
 fs.readFile('/');
 }
 
 deeplyNested();
 
 

The stack trace shows only a limited amount of detail about the exception, and it doesn’t include full information on the call site where the exception originated:

 

 fs.js:60
      throw err;  // Forgot a callback but don't know where? Use NODE_DEBUG=fs
      ^
 
 Error: EISDIR: illegal operation on a directory, read
    at Error (native)
 

Without the helpful comment, many programmers see a trace like this and blame Node for the unhelpful error. But, as the comment points out, NODE_DEBUG=fs can be used to get more information on the fs module. Run the script like this instead:

 

NODE_DEBUG=fs node node-debug-example.js
 

Now you’ll see a more detailed trace that helps debug the issue:

 

 fs.js:53
        throw backtrace;
        ^
 
 Error: EISDIR: illegal operation on a directory, read
    at rethrow (fs.js:48:21)
    at maybeCallback (fs.js:66:42)
    at Object.fs.readFile (fs.js:227:18)
    at deeplyNested (node-debug-example.js:4:6)
    at Object.<anonymous> (node-debug-example.js:7:1)
    at Module._compile (module.js:435:26)
    at Object.Module._extensions..js (module.js:442:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:311:12)
    at Function.Module.runMain (module.js:467:10)
 

It’s clear from this trace that the problem lies in our file, inside a function on line 4 that was originally called from line 7. This makes debugging any code that uses core modules much easier, and it includes both the filesystem and network libraries such as Node’s HTTP client and server modules.

Using DEBUG

The public alternative to NODE_DEBUG is DEBUG. Many packages on npm look for the DEBUG environment variable. It mimics the parameter style used by NODE_DEBUG, allowing you to specify a list of modules to debug or see all of them with DEBUG='*'. Figure 3 shows a project running with DEBUG='*'.


Figure 3 Running an Express application with DEBUG='*'


If you want to incorporate the NODE_DEBUG functionality into your own projects, use the built-in util.debuglog method:

 
 const debuglog = require('util').debuglog('example');
 debuglog('You can only see these messages by setting NODE_DEBUG=example!');
 

To make custom debug loggers that are configured with DEBUG, you need to use the debug package from npm (https://www.npmjs.com/package/debug). You can create as many loggers as you want. Imagine you’re building an MVC web application. You could create separate loggers for models, views, and controllers. Then, when tests fail, you’ll be able to specify the debug logs that are necessary to debug the specific part of the application. The following listing (found in ch10-testing/debug-example/index.js) demonstrates how to use the debug module.

Listing 3 Using the debug package

 

 const debugViews = require('debug')('debug-example:views');
 const debugModels = require('debug')('debug-example:models');
 
 debugViews('Example view message');
 debugModels('Example model message');
 

To run this example and see the view logs, set DEBUG to debug-example:views:

 

 DEBUG=debug-example:views node index.js
 

One final feature of debug logging is that you can prefix a debug section with a hyphen to remove it from logs:

 

 DEBUG='* -debug-example:views' node index.js
 

Hiding certain modules means you can still use the wildcard, but omit unneeded or noisy sections from the output.

Getting better stack traces

If you’re using asynchronous operations, and that includes anything you’ve written using asynchronous callbacks or promises, then you may run into problems when stack traces aren’t detailed enough. Packages on npm can help you in such cases. For example, when callbacks run asynchronously, Node won’t keep the call stack from when the operation was queued. To test this, create two files, one called async.js that defines an asynchronous function, and another called index.js that requires async.js. This snippet’s called aync.js:

 

 module.exports = () => {
  setTimeout(() => {
    throw new Error();
  })
 };
 

And index.js needs to require async.js:

 
 require('./async.js')();
 

Now if you run index.js with node index.js you’ll get a short stack trace that doesn’t show the caller of the failed function, only the location of the thrown exception:

 

    throw new Error();
    ^
 
 Error
    at null._onTimeout (async.js:3:11)
    at Timer.listOnTimeout (timers.js:92:15)
 

To improve this reporting, install the trace package (https://www.npmjs.com/package/trace) and run it with node -r trace index.js. The -r flag tells Node to require the trace module before loading anything else.

Another problem with stack traces is they can be too detailed. This happens when the trace includes too much detail about Node’s internals. To clear up your stack traces, use clarify (https://www.npmjs.com/package/clarify). Again, you can run it with the -r flag:

 

 $ node -r clarify index.js
    throw new Error();
    ^
 
 Error
    at null._onTimeout (async.js:3:11)
 

clarify is particularly useful if you want to include stack traces in error alert emails for a web application.

If you’re running code intended for browsers in Node, perhaps as part of an isomorphic web application, then you can get better stack traces by using source-map-support (https://www.npmjs.com/package/source-map-support). This can be run with -r, but it also works with some test frameworks:

 

 $ node -r source-map-support/register index.js
 $ mocha --require source-map-support/register index.js
 

The next time you’re struggling with a stack trace generated by asynchronous code, look for tools such as trace and clarify to make sure you’re getting the best out of what V8 and Node can offer.

That’s all for this article. For more, download the free first chapter of Node.js in Action, Second Edition.