Mocking HTTP Payloads in Hapi.js

15 Sep 2020 - Melanie Dworak

Routed HTTP requests are a key element of any Hapi application. Unfortunately, the request/response payloads that Hapi automatically generates cannot be intuitively stubbed out. As a result, it’s difficult to manually write inputs for our unit tests.

Our remaining options are to actually send an HTTP request through our routes (defeating the purpose of this exercise!) or to have Hapi imitate a routed method call using its built-in server.inject().

Getting Started with server.inject

You’ll need to instantiate a new Hapi server. Just stick this with the rest of your declarations at the top of your test file:

const testServer = new Hapi.Server();

Don’t worry about starting the server or anything else — we just need it to exist!

Mocking a Route

Next, we need to create the route[s] that will handle our HTTP requests. Depending on your program structure, it’s possible to simply import your production routes and bind them to the test server. If this will not be possible for you, feel free to write a truncated version of the actual route associated with the HTTP requests. A truncated route should at minimum look something like:

testServer.route({
    method: ‘POST’,
    path: ‘/requestRoute’,
    options: {
        handler: methodToTest()
    }
});

Don’t be afraid to include your validators, tags, plugins, or any other route feature you would like to test! The best practice will always be to use the actual production route, but if that’s not possible try to get as close as you can to the real thing!

Putting a Request Through

We call server.inject() to put our HTTP request through and get Hapi’s returned ResponseObject.

server.inject can be called two different ways. The first:

server.inject(‘request-path’);

This version of server.inject() puts through a basic GET request to the given request-path. You likely won’t need to use this version much, given its inflexibility.

The second way to call server.inject() (and the one you probably want):

server.inject(injectOptions);

We define injectOptions separately; it allows us to customize the request in whatever way we might need. It’s usually a good practice to define a request’s injectOptions within each test.

Defining injectOptions

injectOptions will define the specifics of server.inject()’s HTTP request. Here’s an injectOptions template:

const injectOptions = {
    method: ‘HTTP REQUEST TYPE’,
    url: ‘/request-path’,
    payload: {
        Put in the JSON object you’d like to pass through like:
        name : ‘objectName’,
        value : ‘actualValue’,
        etc.
    }
    headers: {‘my-header’: ‘header-value’}
};

Of course, feel free to pick and choose which parts of injectOptions your HTTP request actually needs!

To inject query parameters, you’ll need to include them directly in injectOption’s request-path. Server.inject() will not auto-fill query parameters into a route. For example, if you have a route with path:

path: ‘/delete-object/{id?}’

Hitting that route with injectOptions will looks something like:

const injectOptions = {
    method: ‘DELETE’,
    url: ‘/delete-object/86’
};

Testing server.inject()’s Return Value

server.inject() returns a Hapi ResponseObject. To access the payload and test specific values, you can do something like:

const response = await testServer.inject(injectOptions);
const payload = JSON.parse((response.payload));
expect(payload.name).toBe(‘ExpectedName’);

Putting it All Together

In this article, you have learned how to artificially inject custom Hapi requests into routed methods. You should now be able to make requests with any method, header, url, and payload combo that you need. Hapi testing!

const testServer = new Hapi.Server();
const testController = new TestController();
describe(‘put tests’, () => {
    //a beforeAll is a great place to declare the routes you’ll need for any tests within a describe block!
    beforeAll( () => {
        testServer.route( {
            method: ‘PUT’,
            path: `/${testObjectType}/{id}`
            options: {
                handler: testController.put()
            }
        });
    });

    it(‘should update testObject if the id exists’, () => {
        const injectOptions = {
            method: ‘PUT’,
            path: ‘/myObject/80’,
            payload: {
                name: ‘newName’
            }
        }
        const response = await testServer.inject(injectOptions)l
        const payload = JSON.parse((response.payload));
        expect(payload.name).toBe(‘newName’);
    });
});