Introducing hapi's testing utilities

The test runner in the hapi ecosystem is called lab. If you're not familiar with test runners, they are a Command-line Interface (CLI) tool for running your testing suite. It is inspired by a similar test tool called mocha, and in fact, initially began as a fork of the mocha codebase. But as hapi's needs diverged from the original focus of mocha, lab was born.

code is the assertion library commonly used in the hapi ecosystem. An assertion library forms the part of a test that performs the actual checks to judge whether a test case has passed or not, for example, checking if the value of a variable is true after an action has been taken.

Let's look at our first test script, then we can take a deeper look at lab and code, how they function under the hood, and some differences they have with other commonly-used libraries such as mocha and chai.

Installing lab and code

You can install lab and code the same way as any other module on npm:

npm install lab code –-save-dev

Note the --save-dev flag added to the preceding install command. Remember your package.json file, which describes an npm module? This adds the modules to the devDependencies section of your package.json file. devDependencies are dependencies that are required for the development and testing of a module, but are not required for using the module.

The reason why these are separated is that when we run npm install in an application codebase, it only installs the dependencies and devDependencies of the package.json in that directory. For all the modules installed, only their dependencies are installed, not their development dependencies. This is because we only want the dependencies required to run that downloaded application; we don't need all the development dependencies for every module downloaded as well.

Note

The command npm install installs all the dependencies and devDependencies of package.json in the current working directory, and only the dependencies of those get installed, not devDependencies. To get the development dependencies of a particular module, navigate to the root directory of the module and run npm install.

After you have installed lab, you can then run it with:

./node_modules/lab/bin/lab test.js

This is quite long to type every time, but fortunately, due to a handy feature of npm called npm scripts, we can shorten it. If you look at package.json generated by the npm init in the first chapter, depending on your version of npm, you may see the following within (some code removed for brevity):

…
"scripts": {
  "test": "echo "Error: no test specified" && exit 1"
},
…

The npm scripts are a list of commands related to the project; they can be for testing like we will see in this example, for starting an application, for build steps, and for starting extra servers, among many other options. They offer huge flexibility in how these are combined for managing the scripts related to a module or application. I could spend a chapter or even a book on just these, but they are outside the scope of this book, so let's just focus on what is important to us here. If you would like to learn more, the hoodie team have an excellent comprehensive introduction to npm scripts at https://github.com/hoodiehq/hoodie-css/blob/feature/build-automation/DEVELOPMENT.md.

To get a list of available scripts for a module application, run the following command in the project directory:

$ npm run

To then run any of the listed scripts, such as test, you can just use the following command:

$ npm run test

As you can see, this gives a very clean API for scripts and the context for each of them in the project's package.json. From this point on in this book, most code snippets will use npm scripts for testing or running any examples. We should strive to use these in our projects to simplify and document the commands related to applications and modules for ourselves and others.

Let's now add the ability to run a test file to our package.json. This just requires modifying the scripts section as follows:

…
"scripts": {
  "test": "./node_modules/lab/bin/lab ./test/index.js"
},
…

Note

It is a common practice in Node to place all tests within the test directory of a project.

A handy addition to note here is that when calling a command with npm run, the bin directory of every module in your node_modules directory is added to PATH when running these scripts; so, we can actually shorten this script to:

…
"scripts": {
  "test": "lab ./test/index.js"
},
…

If you remember the last chapter where we used rejoice to launch our applications, we could also have created an npm script to start our application. As in the preceding example, taking advantage of the bin directory of every module in the node_modules directory being added to PATH, this could also have been shortened as follows:

…
"scripts": {
  "start": "rejoice –c index.js –p ."
},
…

Hopefully, this demonstrates the advantage of using npm scripts. Once inside the application directory, a simple call to npm run, and they are given the commands to start the application, or run it's testing suite, like a free form of documentation. You can imagine how, when looking at a new project or module, developers especially, might not know what test runner is used by this project, or what rejoice is, or how to normally start a project. When the npm scripts are in place, as shown in this section, people new to the project don't have to be aware of what the underlying tools are unless they want to.

Local versus global modules

When npm modules are installed like we saw in the previous section, run the following command:

$ npm install lab

It is considered local, as the module is installed within the project directory from which the command is being run. While I believe this is how we should all install our modules, it is worth pointing out that it is possible to also install a module globally. This means that when installing something such as lab, it is immediately added to PATH and can be run from anywhere. We do this by adding a -g flag to the install:

$ npm install lab -g

This may appear handier than having to add npm scripts or run commands locally, outside of an npm script, but it should be avoided where possible. Often installing globally requires sudo to run, which means you are taking a script from the internet, and allowing it to have complete access to your system. Hopefully, the security concerns here are obvious.

Other than that, different projects may use different versions of test runners, assertion libraries, or build tools, which can have unknown side effects, and cause debugging headaches.

The only time I would use globally installed modules are for command-line tools that I may use outside a particular project, such as a Node-based terminal text editor such as slap (https://www.npmjs.com/package/slap) or a process manager such as PM2 (https://www.npmjs.com/package/pm2)—but never with sudo!

Now that we are familiar with installing lab and code and the different ways of running it inside and outside of npm scripts, let's look at writing our first test script, and take a more in-depth look at lab and code.

Our first test script

Let's take a look at what a simple test script in lab looks like, using the code assertion library. Let's create a fresh directory for the examples in this chapter. Create a directory called example-1, and within that another directory called test. As mentioned earlier, in Node, generally all tests are located within the /test directory of a project. Let's create a file called index.js within our new /test directory, and add the following:

const Code = require('code');                    [1]
const Lab = require('lab');                      [1]
const lab = exports.lab = Lab.script();          [2]
lab.experiment('Testing example', () => {        [3]
  lab.test('fails here', (done) => {             [4]
    Code.expect(false).to.be.true();             [4]
      return done();                             [4]
  });                                            [4]
  lab.test('passes here', (done) => {            [4]
    Code.expect(true).to.be.true();              [4]
    return done();                               [4]
  });                                            [4]
});

This preceding script, even though small, includes a number of new concepts; so let's go through it with reference to the numbers given with the code:

  • [1]: Here we just include the code and lab modules as we would any other Node module.
  • [2]: As mentioned before, it is a common convention to place all test files within the test directory of a project. However, there may be JavaScript files in the test directory that aren't tests, and therefore, should not be tested. To avoid this, we inform lab of which files are the test scripts by calling Lab.script() and assigning the value to lab and exports.lab.
  • [3]: The lab.experiment() (aliased lab.describe()) is just a way to group tests neatly. In test output, tests will have the experiment string prefixed to the message, for example, "Testing example fails here". However, this is optional.
  • [4]: These are the actual test cases. Here we define the name of the test and pass a callback function with the parameter function done. We see the assertion library code in action here for managing our assertions. And finally, we call the done function when finished with our test case.

The main things to note here are that lab tests are always asynchronous. In every test, we have to call done() to finish the test; there is no counting of function parameters or checking if the synchronous functions have completed to ensure that a test is finished. Although this requires the boilerplate of calling the done function at the end of every test, it means all tests, synchronous or asynchronous, have a consistent structure.

In chai, which was originally used for hapi, some of the assertions such as .ok, .true, and .false used properties instead of functions for assertions, while assertions such as .equal() and .above() used functions. This type of inconsistency leads to easily forgetting that an assertion should be a method call, and omitting the (). This means the assertion is never called, and the test may pass as a false positive. code's API is more consistent in that every assertion is a function call; here is a comparison of the two:

  • Chai:
    expect('hello').to.equal('hello');
    expect(foo).to.exist;
  • Code:
    expect('hello').to.equal('hello');
    expect('foo').to.exist();

Notice the difference in the second exist() assertion. In chai, you see the property form of the assertion, while in code you see the required function call here. Through this, lab can make sure that all assertions within a test case are called before done is complete, or it will fail the test.

So lets try running our first test script. As we already updated our package.json script, we can run our test with:

$ npm run test

And this will generate the following output:

Our first test script

There are a couple of things to note from this. Tests run are symbolized with a . or an X, depending on whether it passed or not. You can have lab list the full test title by adding the -v or –-verbose flag to our npm test script command.

Note

There are lots of flags to customize the run and output of lab, so I recommend using the full labels for each of these, for example, --verbose, and --lint instead of -v or -l to save you from referring back to the documentation each time.

You may have noticed the No global variable leaks detected message at the bottom. lab assumes the global object shouldn't be polluted, and checks that no extra properties have been added after running tests. lab can be configured to not run this check, or whitelist certain globals. Details of this are in the lab documentation at https://github.com/hapijs/lab.

Testing approaches

I mentioned earlier the concept of TDD or Test-driven Development. This is one of the many approaches known for building a test suite, as is BDD (Behavior-driven Development), and like most test runners in Node, lab is unopinionated in how you structure your tests. Details of how to structure your tests in a TDD or BDD can again be found easily in the lab documentation.

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

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