Tuesday, October 28, 2014

Getting Started with Hapijs and Testing

API driven development allows for easy testing of the backend and a variety of front-end options. Hapijs for nodejs is a great solution when taking this approach. This is a step-by-step tutorial on Getting Started with Hapijs and Testing.

This tutorial takes a different approach than most. Testing is important for serious developement. If you think coding should start with a test, then you'll like this tutorial because that is how it starts.

Note: updated for Hapi version 8.x

Installing Hapijs


To start with testing not only do we need to install hapijs but also a few plugins. Installation is still very easy. And like other nodejs projects, a package.json file gets created.

First, create a directory for this tutorial. I use the name "hapijstut". You can choose any label. Then move into that directory.

Type the following to create your package.json file. But don't enter the defaults when prompted (see below):

npm init

When prompted for the test command enter the following. For the other prompts, default is fine.

./node_modules/lab/bin/lab -c

Next we want to install hapi and the joi plugin that we'll be using shortly. So type this:

npm install hapi joi --save

Finally, install the "lab" and "code" plugins needed for testing. Since these are not required for production the --save argument is now "--save-dev".

npm install lab code --save-dev

This should result in a package.json file that looks like this:

{
  "name": "hapijstut",
  "version": "0.0.0",
  "description": "Getting Started with Hapijs",
  "main": "index.js",
  "scripts": {
    "test": "./node_modules/lab/bin/lab -c"
  },
  "author": "Your Name ",
  "license": "BSD-2-Clause",
  "dependencies": {
    "joi": "~4.7.0",
    "hapi": "~7.1.1"
  },
  "devDependencies": {
    "code": "~1.2.0",
    "lab": "~5.0.1"
  }
}


Setting Up a Hapi Test


Code tests are actually pretty simple. Describe the expected result and see if the actual result matches. Or if it doesn't match.

Setting up code tests is work because you have to write code. But after you've written a few, it gets easy. That's why it is a good idea to start learning Hapijs by coding some tests. Let's setup tests.

Create a new directory in our hapijs project directory ("hapijstut"). It must be called "test" ("hapijstut/test"). In that directory you'll create a new file called "first.js" which uses the Lab plugin for testing hapijs code.


var Lab = require("lab");
var lab = exports.lab = Lab.script();
lab.experiment("Hello", function() {
    // tests
});

This test needs more coding, but we can run it to make certain that test runs. So on the command line type:

npm test

You should see a result of "0 tests complete".

0 tests complete
Test duration: 0 ms
No global variable leaks detected
Coverage: 0.00% (0/0)

We can run tests, now lets code a test.


The Shell For A Hapi Test


Before a test can be code a shell is needed to hold the test.

The test code is placed inside the function that is an argument of lab.experiment. Lab.experiment is a way to group tests. Even though we have only one test, it is a good practice to use lab.experiment because you will add more tests as you develop your application. We'll do so later in this tutorial.

The first argument of lab.experiment is a description of the test.


lab.test("Testing for 'Hello World'",
  function(done){
    //test code here
});


Take these four lines of code and replace the "//test" comment in the first.js file. Then run our test again using "npm test" as we did above.

The result will be:


Failed tests:
  1) Hello Testing for 'Hello World':
    Timed out (2000ms)
1 of 1 tests failed
Test duration: 2004 ms


This shows that tests will timeout after 2 seconds if the code is not written properly. The test code needs to execute the done() function which is passed to it. If you want to see the test pass, you can replace "//test code here" with "done()". Run the test ("npm test") and you'll see that the test will complete.

Success. We've run our first test and it failed! When writing tests, you'll see Failed many times. This is actually how you start testing. Write the test and make certain it fails. Then write the server code and make sure it passes the test. In short, a failure is the first step towards success.


Testing The Server


Below we will code a hapijs server that will repond with "Hello World". To test that server our test code must "see" that server. This requires one line of code. At the begining of our first.js file we add the following line just after "var Lab = require('lab')". This pulls in the server code which will be in the "server.js" file in the parent directory.


var server = require("../server.js");


Now if you run the test, it will fail because the file "server.js" cannot be found.

To make the test pass, let's write the code the server and put it in the file named "server.js" in our project directory.


var Hapi = require("hapi");
var server = new Hapi.Server();
server.connection({ port: 8989 });
var hello = function (request, reply) {
  reply('Hello World');
};
server.route({ method: 'GET', path: '/', handler: hello });
server.start();
console.log('hello server http://localhost:8989');


The above code requires "hapi" just like any nodejs program that uses a module. Then a server is defined to run on port 8989. Next a handler is defined to reply "Hello World" when a request is made to the server. The route is set next which uses the "hello" handler. Finally the server is started and a message is sent to the console.

Run "node server.js" to execute the code and then point your browser at "localhost:8989" to see "Hello World".

Back to testing. The server code works fine but the serve code needs to go into the test code. The method to do this with nodejs is to use the server code as a module. So at the end of the server.js code the server is exported. Here is the revised server.js code:


var Hapi = require("hapi");
var server = new Hapi.Server(8989);
var hello = function (request, reply) {
  reply('Hello World');
};
server.route({ method: 'GET', path: '/', handler: hello });
server.start();
console.log('hello server http://localhost:8989');
module.exports = server;


While the test passes, you'll see that the result shows "missing coverage". Let's now add the test code.


Coding The Test


Our test code will expect to see a request to our server return "Hello World". The Lab plugin needs an assertion library to do this part. This tutorial uses the "Code" plugin - which we added to the project at the start of this tutorial. This gives us the ability to expect a result and see if we got what we expected.

In the code below the function part of lab.test is now complete. The options are set for doing a GET request from the route of "/". The server module which was required in the third line is then injected specifying the options and with a typical responce callback. The code plugin method of expect is used to examine the result of the response and see if it is equal to "Hello World". Hopefully it is and our test can pass. After that line runs successfully then the test is done().


var Lab = require("lab");
var lab = exports.lab = Lab.script();
var server = require("../server.js");
var code = require("code");

lab.experiment("Hello", function() {
  lab.test("Testing for 'Hello World'",
    function(done){
      var options = {method: "GET", url:"/"};
      server.inject(options, function(response){
        var result=response.result;
        code.expect(result).to.equal("Hello World");
        done();
      });
  });
});


Run the test (npm test) and you should see the result of



result="Hello World"
  .
1 tests complete
Test duration: 16 ms
No global variable leaks detected
Coverage: 100.00%

There are actually a few more things that this server should produce. The status code it returns should be a 200. We could test that the length of the string returned is 11. Add these expectations just below the other code.expect line.


code.expect(response.statusCode).to.equal(200);
code.expect(result.length).to.equal(11);

Run the test and everything passes with 100% coverage. This is just what we wanted to see.


Continue Coding So Add Another Test


The "Hello World" server is now working as expected. Time to start coding our next API. For this case we want a request to "/status" to return the "{message: 'ok'} object. So let's write the test first. Add this code to a new file in the "/test" directory and name it "second.js".


var Lab = require("lab");
var lab = exports.lab = Lab.script();
var server = require("../server.js");
var code = require("code");

lab.experiment("Status", function() {
  lab.test("Testing for 'ok' status code.",
    function(done){
      var options = {method: "GET", url:"/status"};
      server.inject(options, function(response){
        var result=response.result;
        console.log('result='+JSON.stringify(result));
        code.expect(result.message).to.equal("ok");
        code.expect(response.statusCode).to.equal(200);
        done();
      });
  });
});

Now when you execute the test (npm test) both first.js and second.js are run. No configuration to modify. Simply add the new test file to the "/test" directory. The test runner looks for any ".js" files in the "/test" directory and runs them.

Of course when you add this our tests will now fail because we've not yet written code for the "/status" route.

1 of 2 tests failed

To pass the test add another route to the server.js file.


var check = function (request, reply) {
  reply({message: "ok"});
};
server.route({ method: 'GET', path: '/status', handler: check });

Note that the hapijs server is now returning an object. Run the test and it will now pass.


Conclusion


As you can see from adding this last bit of code, it is much easier to add additional tests. Hopefully this tutorial has you starting to write tests for your hapijs project and you'll continue to have test coverage for your code.

Other Tutorial

Hapijs for Proxy Routes and mapUri

1 comment:

Unknown said...

I'm fairly new to node and I have never seen tests run with npm before - could you explain how npm can be used to run tests? Thanks!