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
- What You’ll Build
- Prepare Your Project: Stack & Structure
- Environment Variables and Storing Secrets
- Set Up MongoDB and Connect With Mongoose
- Sending Emails in Node.js
- Load the User’s Profile Picture From Gravatar Using Virtuals in Mongoose
- Implement a User Profile Editing Screen
- Generate a Username in Mongoose Middleware
- Displaying Seasons and Episodes for TV Shows with Mongoose Relationship Population
- Implementing Pagination for Movies
- Implement a Watchlist
- Create a Full Text Search with MongoDB
- Create a REST API with JSON Endpoints
- Update Mongoose Models for JSON Responses
- API Pagination for TV Shows
- Customize API Endpoints with Query Parameters
- Always Throw and Handle API Validation Errors
- Advanced API Validation With Custom Errors
- Create an API Documentation with Swagger
- Customize Your Swagger API Documentation URL
- Describe Endpoint Details in Your Swagger API Documentation
- 10 Tips on API Testing With Postman
- JWT Authentication in Swagger API Documentation
- API Versioning with Request Headers
- API Login With Username and Password to Generate a JWT
- JWT Authentication and Private API Endpoints
- Refresh Tokens With JWT Authentication
- Create a JWT Utility
- JWT Refresh Token for Multiple Devices
- Check Refresh Token in Authentication Strategy
- Rate Limit Your Refresh Token API Endpoint
- How to Revoke a JWT
- Invalidate JWTs With Blacklists
- JWT Logout (Part 1/2)
- JWT “Immediate” Logout (Part 2/2)
- A Better Place to Invalidate Tokens
- How to Switch the JWT Signing Algorithm
- Roll Your Own Refresh Token Authentication Scheme
- JWT Claims 101
- Use JWT With Asymmetric Signatures (RS256 & Co.)
- Encrypt the JWT Payload (The Simple Way)
- Increase JWT Security Beyond the Signature
- Unsigned JSON Web Tokens (Unsecured JWS)
- JWK and JWKS Overview
- Provide a JWKS API Endpoint
- Create a JWK from a Shared Secret
- JWT Verification via JWKS API Endpoint
- What is JOSE in JWT
- Encrypt a JWT (the JWE Way)
- Authenticate Encrypted JWTs (JWE)
- Encrypted and Signed JWT (Nested JWT)
- Bringing Back JWT Decoding and Authentication
- Bringing Back JWT Claims in the JWT Payload
- Basic Authentication With Username and Password
- Authentication and Remember Me Using Cookies
- How to Set a Default Authentication Strategy
- Define Multiple Authentication Strategies for a Route
- Restrict User Access With Scopes
- Show „Insufficient Scope“ View for Routes With Restricted Access
- Access Restriction With Dynamic and Advanced Scopes
- hapi - How to Fix „unknown authentication strategy“
- Authenticate with GitHub And Remember the Login
- Authenticate with GitLab And Remember the User
- How to Combine Bell With Another Authentication Strategy
- Custom OAuth Bell Strategy to Connect With any Server
- Redirect to Previous Page After Login
- How to Implement a Complete Sign Up Flow With Email and Password
- How to Implement a Complete Login Flow
- Implement a Password-Reset Flow
- Views in hapi 9 (and above)
- How to Render and Reply Views
- How to Reply and Render Pug Views (Using Pug 2.0)
- How to Create a Dynamic Handlebars Layout Template
- Create and Use Handlebars Partial Views
- Create and Use Custom Handlebars Helpers
- Specify a Different Handlebars Layout for a Specific View
- How to Create Jade-Like Layout Blocks in Handlebars
- Use Vue.js Mustache Tags in Handlebars Templates
- How to Use Multiple Handlebars Layouts
- How to Access and Handle Request Payload
- Access Request Headers
- How to Manage Cookies and HTTP States Across Requests
- Detect and Get the Client IP Address
- How to Upload Files
- Quick Access to Logged In User in Route Handlers
- How to Fix “handler method did not return a value, a promise, or throw an error”
- How to Fix “X must return an error, a takeover response, or a continue signal”
- Query Parameter Validation With Joi
- Path Parameter Validation With Joi
- Request Payload Validation With Joi
- Validate Query and Path Parameters, Payload and Headers All at Once on Your Routes
- Validate Request Headers With Joi
- Reply Custom View for Failed Validations
- Handle Failed Validations and Show Errors Details at Inputs
- How to Fix AssertionError, Cannot validate HEAD or GET requests
- Getting Started With Testing Using Lab and Code
- Run Tests with Assertions using Code
- Test Route Handlers by Injecting Requests
- Inject Request Payload, Headers and Parameters While Testing Route Handlers
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
- hapi’s server.inject(options) method in the API docs
- Getting Started With Testing in hapi Using Lab and Code tutorial on Future Studio
- Assertions with Code tutorial on Future Studio
- Futureflix Starter Kit’s test directory