hapi — Test Route Handlers by Injecting Requests

One of the reasons we’re passionate about hapi is its easiness of testing. The framework has a built-in server.inject method that simulates an incoming HTTP request.

A server.inject call won’t create a socket connection. You’re not relying to the network stack to run tests in your app. That means, testing your app locally is delightful, because you don’t need a complete end-to-end setup consuming a ton of ports due to hapi server instances!

If you haven’t seen the related tutorials on getting started with testing in hapi using lab and code and assertions with code, give them a shot.

hapi Series Overview

How to Inject Requests to hapi

Let’s start with a practical example. You should be familiar with the skeleton of a test in hapi. If not, head over to the getting started with testing in hapi tutorial and get a grasp on it.

Injecting requests into a hapi server is great for testing. You don’t need to create a dedicated hapi server and run it on a port on your local or testing machine. There’s no socket connection required to inject a request into hapi and receive an response. The server.inject() method makes it possible.

The server.inject method accepts to types of parameters:

  • the request URL as a string value
  • an options object containing request details

Let’s look at both ways.

The Quick Way to Inject Requests

To quickly inject a request to a hapi server, provide a request path directly to server.inject('/request-path').

// … imports (lab, code), assignments, export lab script, etc. :)

describe('inject requests with server.inject,', () => {  
  it('quickly injects a request', async () => {
    const server = new Hapi.Server()

    // wait for the response and the request to finish
    const response = await server.inject('/request-path')
  })
})

In the code snippet above, hapi performs a GET request on the /request-path URL path. To run more complex requests, use an options object which is described in the upcoming subsection.

Inject Requests Based on an Options Object

Have a look at the following code snippet that outlines a basic test. The test creates a new hapi server and directly injects a GET request to /.

We expect this request to pass the request lifecycle and hapi to respond.

'use strict'

// … imports (lab, code), assignments, export lab script, etc. :)

describe('inject requests with server.inject,', () => {  
  it('injects a request to a hapi server without a route', async () => {
    const server = new Hapi.Server()

    // these must match the route you want to test
    const injectOptions = {
      method: 'GET',
      url: '/'
    }

    // wait for the response and the request to finish
    const response = await server.inject(injectOptions)

    // alright, set your expectations :)
    expect(response.statusCode).to.equal(404)
  })
})

You can copy and paste this test to your environment and run it. It’ll pass, because the newly created hapi server instance doesn’t serve this route. That’s why it responds with Not Found 404.

Notice that the injected request to a hapi server bases on the provided options. Let’s have a look at available options that you can assign with a request injection.

Options For server.inject

Anytime you’re passing an options object to a method, you don’t necessarily know the available fields. In such cases head over to the hapi API documentation and look out for the related method. In this situation, you’re heading for the section of server.inject(options).

Scrolling through the available options you’ll notice that there’s a single required option: url. Everything else has a default or null value which works in the majority of use cases.

For example, the method field defaults to GET and we could remove the method: 'GET' in the example above. That comes with knowledge depth and not every developer might know this default value. Be explicit when possible.

Have a look at the linked API section in hapi’s docs for server.inject. It allows you to inject request headers, params and payload. You can also pass credentials to bypass authentication or adjust the plugin states for the route to call. We’ll look at these scenarios in upcoming tutorials 😃

Inject a Request to a Route Handler

The previous example is at a bare minimum. Let’s look at a more complex example and add a route with a request handler to the hapi server. That gets us closer to a real situation where you’re adding your routes to a hapi server and testing the handlers by injecting requests.

The difference in this example is that you’re writing the route handler within the test itself. That’s probably not the case in your hapi app 😉

03-inject-requests.js

const Lab = require('lab')  
const Code = require('code')  
const Path = require('path')  
const Hapi = require('hapi')

// Test files must require the lab module, and export a test script
const lab = (exports.lab = Lab.script())

// shortcuts from lab
const { describe, it } = lab

// shortcuts from code
const expect = Code.expect

describe('inject requests with server.inject,', () => {

  it('inject a request', async () => {
    const server = new Hapi.Server()
    server.route({
      method: 'GET',
      path: '/',
      handler: () => {
        return {
          name: 'Marcus',
          isDeveloper: true,
          isHapiPassionate: 'YEEEEAHHH'
        }
      }
    })

    // these must match the route you want to test
    const injectOptions = {
      method: 'GET',
      url: '/'
    }

    // wait for the response and the request to finish
    const response = await server.inject(injectOptions)

    // alright, set your expectations :)
    expect(response.statusCode).to.equal(200)

    // shortcut to payload
    const payload = JSON.parse(response.payload)
    expect(payload.name).to.equal('Marcus')

    // of course you can assign more “expect” statements
  })
})

The route registration with server.route tells the server instance to listen for requests at GET / and respond with a JavaScript object consisting of name, isDeveloper and isHapiPassionate fields.

When testing this specific route, the options for server.inject need to match the route’s options. Pass the same HTTP method (GET) and path (/) to server.inject(injectOptions) as defined in server.route(routeOptions).

Ultimately, wait for the response and set your expectations. Pun intended 😅

This kind of testing with a route handler in the test itself is a good way to test your hapi plugins that extend the request lifecycle.

If you’re writing a hapi plugin that extends, e.g. onPreHandler, and you want to test it, register that plugin to the hapi server instance and let a handler return a value that you’d assert as a result. This way you can make sure your plugin works correctly.

Find the Test Files in the Futureflix Starter Kit

There’s a dedicated test file with the examples from this tutorial in the Futureflix Starter Kit’s test directory.

Look out for the file called 03-inject-requests.js. There you find this tutorial’s code snippets in a single file.

Outlook

Hopefully you can see the benefits of injecting requests in hapi. It allows you to run an integration test of route handlers where a request passes the request lifecycle as any actual request would do.

Have a look at all the options that server.inject(options) accepts to customize the payload, headers, params, credentials, and behavior of the injected request. We’ll walk through the details on injecting data with requests in the next tutorial 😉

We’re happy to hear your thoughts and appreciate your comment. Please don’t hesitate to use the comment form below. If Twitter is more your thing, find us @futurestud_io. Let us know what you think!

Enjoy coding & make it rock!


Mentioned Resources

Explore the Library

Find interesting tutorials and solutions for your problems.