Hapi is our favorite Node.js web framework. One of the reasons we love hapi is the easiness of testing. The built-in server.inject
method helps you to follow a test-driven development approach.
This tutorial gets you started with testing in hapi. You’ll set up your project to run tests with npm test
and receive all wordings and concepts in testing with lab
and code
from hapi’s ecosystem.
Once you’re on board with testing, you won’t “just skip tests for this feature, it’ll work fine” 😉
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
Install Dependencies: lab
and code
Lab
is a Node.js testing framework within the hapi ecosystem. We’ll use lab for testing and within the code samples to illustrate testing in hapi. You can replace lab with any other library, like AVA, tape, or mocha.
Additionally to lab as a test framework, we’ll use the assertion library code
. Again, you can replace code
with libraries like chai.
Install lab
and code
and save the packages as devDependencies
:
# Running Node.js v8+
npm i -D lab code
# Running Node.js v6-
npm i -D lab@14 code@4
Notice that the latest releases of lab
and code
are only compatible with Node.js v8+. If you’re on Node.js v4 or v6, use lab
in version 14 and code
in version 4.
Set Your Project Up for Testing
When following the traditional TTD, you’ll write your tests first. Ensure a solid feature implementation by defining and creating the tests first.
You can surely add tests to an existing project, no need to start from the green field.
We’ll use the Futureflix Starter Kit in this tutorial to illustrate the setup of tests. You’ll go ahead with your own project 😃
Create a Dedicated test
Directory
The first task is to create a test
directory in your project’s root.
Let’s say your project files are in a directory called futureflix-starter-kit
. Add a test
folder in your project’s root directory. If the test
folder already exists, go ahead and use it. You don’t need to create a folder like test-new
, keep the existing one.
futureflix-starter-kit
|_ public
|_ test <-- add this folder
|_ …
|_ web
|_ package.json
|_ server.js
You’ll place all your test files within the test
directory. Read the next section to prepare the npm test
command in your package.json
file to use lab
for testing.
Update package.json
To Run Your Tests with Lab
NPM allows you to define commands. Run them with the npm
command line utility. Common commands are npm install
, npm start
and also npm test
.
Make use of these command NPM shortcuts. Open your project’s package.json
file and search for the script
tag. Look out for the test
key and replace the default echo \"Error: no test specified\" && exit 1
value by lab --assert code --leaks --coverage
.
{
"name": "futureflix",
…
"scripts": {
"test": "lab --assert code --coverage"
}
}
Now, when running npm test
, NPM evaluates and runs the test
command from the scripts
block in your package.json
file. NPM aliases the lab
command from node_modules
so you don’t need to define node_modules/lab/bin/lab.js --assert …
.
The flags --assert
and --coverage
tell lab to enable these options while running the tests.
With --assert code
you’re defining code
as the library for assertions. You won’t read further details on code
in this tutorial, but you should install and add it within the test
command already. The next tutorial goes all in on assertions with code
. Hope you stay hungry 😉
The flag for --leaks
enables memory leak detection in lab and warns you if it detects one. The --coverage
flag prints the code coverage besides the test results. You don’t necessarily need to cover 100% of your code with tests. 100% code coverage doesn’t mean your project is free of bugs and your tests cover every use case. Write tests to cover the important parts of your app and the coverage outline will help you with that.
Run npm test
Again to Verify the Setup
Run npm test
to verify your setup. Lab loads and runs every file located within the test
folder. At this point, you don’t have any tests. You command line output should look like this:
$ npm test
> futureflix@1.1.0 test /Users/marcuspoehls/Dev/FutureStudio/futureflix-starter-kit
> lab --assert code --leaks --coverage
The pattern provided (-P or --pattern) didn't match any files.
Nothing to do for lab. Time to create your first test!
Create your First Test
The test
directory is in place and the NPM script to run tests makes use of lab. There’s nothing to do yet for lab.
Because we’re using the Futureflix Starter Kit to illustrate the test setup, let’s put all test files in a dedicated folder inside the test
directory. Call the dedicated folder getting-started
.
Now, create a new file in the test/getting-started
directory. Name it 01-getting-started.js
. Well, the file name is creative. To spoiler, there will be a test in this file that always succeeds. You’ll get to know the first part of lab’s building blocks 😁
Within your building-blocks.js
test file, import and assign variables for lab
and code
.
01-getting-started.js
'use strict'
const Lab = require('lab')
// Test files must require the lab module, and export a test script
const lab = (exports.lab = Lab.script())
// shortcuts to functions from lab
const experiment = lab.experiment
const test = lab.test
experiment('getting started with hapi testing,', () => {
test('TODO')
})
Notice the exports.lab = Lab.script()
. It’s a requirement to export a lab script when running tests with lab.
Besides lab’s export, you can see the creation of two shortcuts: experiment
and test
. Read more about them in the next paragraph.
Group Tests Into Experiments
If you want to group tests by category or topic, use lab’s experiment
.
experiment('test the sign up,', () => {
test('sign up fails without email address')
test('sign up fails without password')
})
experiment('test the login,', () => {
test('login fails without email address')
test('login fails for not registered email address')
})
Let’s illustrate a use case of experiments to get a grasp on it. Assume you’re writing a hapi plugin that handles user signup and log in. The sign-up and login are separate features. You can use a single test file and test both features in there.
Separate the tests for the sign up within a dedicated experiment from the login tests. You can also create different test files to isolate the testing for sign up and log in. Experiments are a good way to structure your tests by features.
Assign each experiment a title, like test the sign-up,
(the comma at the end is no typo). Lab concatenates this title with the test’s title and show it for failing tests. We use a comma at the end of the experiment’s title as a separator between the two titles.
Create a Test
Finally, you can use the test
shortcut and create an actual test. The test
shortcut is a function that takes three optional arguments:
The title
as the first parameter. The second parameter is an options
object. More about the options in the next section. The third parameter is a callback(done)
function providing another done(err)
callback itself.
If you don’t provide the callback function when calling test()
, lab considers it as a TODO and skips this test. The test above is one that lab skips:
experiment('getting started with hapi testing,', () => {
test('lab considers this as a TODO')
})
To complete a test successfully, call done()
, without anything, no error, no data, nothing. Passing data or an error to the done
callback makes the test fail.
Let’s create a second test that lab won’t skip. This test will always succeed because you’re calling done()
right away:
experiment('getting started with hapi testing,', () => {
test('lab considers this test as TOOD and skips it')
test('always succeeding :)', () => {})
})
The second test has the done
callback and that means you need to call it to tell lab
the test fails or succeeds. If you don’t call done(err)
, it’ll time out after lab’s timeout interval. The default timeout for a test in lab is 2,000ms.
Options for Tests and Experiments
Both, the test
and experiment
functions accept the same options. You can pass an options object as the second argument to both functions.
The allowed options are:
timeout
: set a specific timeout in milliseconds. Defaults to 2,000 ms or the value of-m
parallel
: activates parallel execution of tests within each experiment level. Defaults tofalse
(serial execution)skip
: skip execution. Cannot be overridden in children once you set up the parent to skiponly
: marks all other tests or experiments with skip
Provide an options object to the experiment
that runs every test in parallel:
experiment('getting started with hapi testing,', { parallel: true }, () => {
test('lab considers this test as TODO and skips it')
test('always succeeding :)', { timeout: 5000 }, () => {})
})
Another way to set the only
and skip
options is to chain methods with the same name, like this:
experiment('getting started with hapi testing,', { parallel: true }, () => {
test.skip('lab considers this test as TODO and skips it')
test('always succeeding :)', { timeout: 5000 }, () => {})
})
You can’t use .parallel()
or .timeout()
on tests and experiments. To define these options, use the options object.
Run Your Tests: npm test
The last time you ran npm test
, there was no test file to execute. With the newly created test, run it again and verify whether your code works properly.
If everything is fine, you should see an output like this:
$ npm test
> futureflix@1.1.0 test /Users/marcuspoehls/Dev/FutureStudio/futureflix-starter-kit
> lab --assert code --leaks --coverage
-.
1 tests complete (1 skipped)
Test duration: 8 ms
Assertions count: 0 (verbosity: 0.00)
Coverage: 0.00% (0/0)
The 01-getting-started.js
test file contains a single experiment with two tests. Lab skips the first test because you didn’t define a done(err)
callback. The second test succeeds.
You made it through the initial setup. You know that this is the beginning of testing your hapi application. For today, you made great progress! 🤘
Outlook
Your hapi project is ready for test-driven development 😉 The hapi ecosystem provides a Node.js test framework called lab
. We trust and like lab, it’s a great tool.
If you want to use another testing tool, replace lab with your favorite one.
Testing with hapi is enjoyable. The next tutorial on testing in hapi shows you how to use code
to assert results.
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
- lab Node.js test framework on GitHub
- code assertion library on GitHub
- AVA JavaScript test runner
- tape testing for Node.js and the browser
- mocha test framework for Node.js and the browser
- chai assertion library
- Test-Driven-Development Wiki article
- Futureflix Starter Kit on GitHub with test samples