|
From Node.js in Action, Second Edition 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. |
This article was originally posted here: https://www.sitepoint.com/testing-node-applications/
Save 37% on Node.js in Action, Second Edition. Just enter code fcccantelon into the discount code box at checkout at manning.com.
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 in terms of software testing. 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. You might also want to look into automation testing that could help test your software and scripts. 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(`My to-do list Welcome to my awesome to-do list
`); }); 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.
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.(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, check out the whole book on liveBook here.