# Set up an Async Fastify App with Jest

Proof of concept to test a Fastify App which uses asynchronous plugins.

- **Author:** Patrick Heneise
- **Published:** 2021-09-22
- **Updated:** 2021-09-22
- **Tags:** fastify, test, jest

---

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](https://jaywolfe.dev/blog/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`](https://github.com/nearform/fastify-secrets-gcp). The
plugin fetches information from the
[Google Cloud "Secret Manager"](https://cloud.google.com/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](https://www.fastify.io/docs/latest/Testing/#separating-concerns-makes-testing-easy),
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](https://www.fastify.io/docs/latest/Testing/#separating-concerns-makes-testing-easy).

**app.js:**

```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**

```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:

```js
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](https://jaywolfe.dev/blog/setup-a-fastify-app-with-jest-tests-the-right-way/#swapping-tap-for-jest---test-setup)
_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`](https://jestjs.io/docs/configuration#testenvironment-string)
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](https://stackoverflow.com/q/61974052):

**.jest/fastify-env.js**:

```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**:

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

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

**eslintrc.js**

```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:

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

**package.json**

```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](https://github.com/zentered/demo-async-fastify-with-jest), please
ping us on Bluesky [@zenteredco](https://bsky.app/profile/zentered.co) if you
have questions or comments.
