article

Set up an Async Fastify App with Jest

22 Sep 2021 | 4 min read

official fastify logo

First things first: this article is not about jest being better than tap. Both are great testing frameworks. Jest comes with most front-end frameworks these days and we wanted to stay consistent, so we decided to go use it with Fastify, which is definitely the coolest and fastest Node.js framework right now :).

For any standard (synchronous) Fastify app, you can follow Jay Wolfes guide on how to Setup A Fastify App with Jest Tests the Right Way.

We recently installed the first asynchronous Fastify plugin in one of our projects: fastify-secrets-gcp. The plugin fetches information from the Google Cloud "Secret Manager", a great system to store sensitive information in the environment. What makes this plugin so different is the fact that you can only use it after await fastify.ready(), which makes it somewhat tricky to work with. For example fastify-auth0-verify requires the auth0 credentials for the registration of the plugin, and something like the "client secret" surely belongs into the Secret Manager. So we need to wait for fastify-secrets-gcp to be ready and then continue the registration of the other plugins, all before fastify is ready to serve requests.

As recommended by Fastify in their testing guide, you should separate the http server code, and the fastify app, if you haven't done so.

TLDR; Show me the code please

Here's a proof of concept repo with everything you need: https://github.com/zentered/demo-async-fastify-with-jest

Async Example with GCP Secrets and Auth0

Based on the official testing example from the docs.

app.js:

'use strict'
const Fastify = require('fastify')
const FastifySecrets = require('fastify-secrets-gcp')
async function build(opts = {}) {
const app = Fastify(opts)
const secrets = {
auth0ClientSecret: `projects/${process.env.GCLOUD_PROJECT_ID}/secrets/AUTH0_CLIENT_SECRET/versions/latest`
}
await app.register(FastifySecrets, {
secrets: secrets
})
app.register(require('fastify-auth0-verify'), {
domain: auth0.domain,
audience: auth0.audience,
secret: app.secrets.auth0ClientSecret
})
app.get('/', async function (request, reply) {
return { hello: app.secrets.auth0ClientSecret }
})
return app
}
module.exports = build

By waiting for app.register(), app.secrets is available in the following auth0 plugin registration.

server.js

'use strict'
const build = require('./app')
const start = async () => {
try {
const fastify = await build({})
await fastify.listen(3000)
} catch (err) {
console.log(err)
process.exit(1)
}
}
start()

Instead of calling fastify.listen() straight away, we need to wait for the build, and then initiate the server.

The Problem

Every time you want to launch your app, you need to await build(). This includes all tests. You can of course do this:

const build = require('../app')
test('async example', async () => {
const fastify = await build()
const response = await fastify.inject({
method: 'GET',
url: '/'
})
expect(response.statusCode).toEqual(200)
})

But with a growing number of routes, plugins, hooks etc., building the app in every single test can will be painfully slow.

Solution: Jest testEnvironment

Theoretically a Jest helper as described in Jay Wolfes article should work with a little tweaking. We tried several approaches and the beforeAll hook wouldn't build the app before the test runs, so we gave up on this.

The testEnvironment worked fairly straight forward. You can define a custom test environment and initialize an (async) app for the entire testbase. Inspired by this question on StackOverflow:

.jest/fastify-env.js:

const NodeEnvironment = require('jest-environment-node')
const fastifyBuilder = require('../app')
class FastifyEnvironment extends NodeEnvironment {
async setup() {
await super.setup()
const fastify = await fastifyBuilder({})
this.global.fastify = fastify
}
}
module.exports = FastifyEnvironment

Then just tell Jest to use the custom environment:

jest.config.js:

{
testEnvironment: './.jest/fastify-env'
}

Also, fastify is now a global variable and needs to be defined, otherwise ESLint complains:

eslintrc.js

globals: {
fastify: 'readonly'
}

Exception: unit tests

If you have unit and integration tests, some calling fastify.inject(), others not, you can use the following snippet in jest.config.js to load the custom environment only in integration tests where the jest environment is needed:

if (process.env.npm_lifecycle_event === 'test:unit') {
jestConfig.testEnvironment = 'jest-environment-node'
} else {
jestConfig.testEnvironment = './.jest/fastify-env'
}

package.json

{
"scripts": {
"test": "jest",
"test:unit": "jest --testPathIgnorePatterns \"/(routes|workflows)/\""
}
}

Conclusion

Thanks for reading, we hope you find this useful. Here's the GitHub Code, please ping us on Twitter @zenteredco if you have questions or comments.

Patrick Heneise

Chief Problem Solver and Founder at Zentered

I'm a Software Enginer focusing on web applications and cloud native solutions. In 1990 I started playing on x386 PCs and crafted my first “website” around 1996. Since then I've worked with dozens of programming languages, frameworks, databases and things long forgotten. I have over 20 years professional experience in software solutions and have helped many people accomplish their projects.

pattern/design file