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.