Repository: mercurius-js/mercurius
Pull request #985 (2023-04-24 10:49:45)

feat: added onExtendSchema application hook
Added `onExtendSchema` hook, as requested in https://github.com/mercurius-js/validation/issues/39.

An issue with the `extendSchema` function is that, because the hook handler is called inside the function and returns a promise, waiting for the promise will cause `extendSchema` to become async as well, which would be a breaking change. Does this make sense?

simoneb: @stearm thanks for this PR. can you take a look why CI is red?
stearm: > 

The reason is that now there isn't a 100% coverage, because of [this](https://github.com/mercurius-js/mercurius/pull/985/files#diff-e727e4bdf3657fd1d798edcd6b099d6e092f8573cba266154583a746bba0f346L242) update.

I'm trying to test the left part of the `||` but actually, it seems to be never called.
Before my update it was:

`fastifyGraphQl.extendSchema = fastifyGraphQl.extendSchema || function (s) { ... }`

But since the `graphql` field is created by mercurius itself, I don't think there is a chance that `extendSchema` is already defined. If this is true, the implementation becomes:

```
fastifyGraphQl.extendSchema = (s) => {
    if (typeof s === 'string') {
      s = parse(s)
    } else if (!s || typeof s !== 'object') {
      throw new MER_ERR_INVALID_OPTS('Must provide valid Document AST')
    }

    fastifyGraphQl.schema = extendSchema(fastifyGraphQl.schema, s)
    
    const context = assignApplicationHooksToContext({}, fastifyGraphQl[kHooks])

    if (context.onExtendSchema !== null) {
      onExtendSchemaHandler({ schema: fastifyGraphQl.schema, context })
    }
  }
```

and so the coverage will become green again. Wdyt @simoneb ?
simoneb: Thanks for this PR @stearm. A few comments:

- I think we need to preserve the existing behavior that, if `extendSchema` already exists, we don't re-define it. It's unlikely to happen as you said, but there's no reason to remove this behavior either
- the gateway package definitely overrides this method because apparently it's doing something with the schema which doesn't allow calling `extendSchema` after you register the gateway, which I guess is fine and we shouldn't do anything about it
- about the asynchronous nature of this hook, good point. I suspect that this will require making `extendSchema` async, which would be breaking change. Before we go that far, I think it would be worth checking something that may have been done in the past around this. More specifically, this looks like the first application lifecycle hook (as opposed to a request lifecycle hook) that we're implementing. In the docs about hooks, I can see at the bottom of the page a reference to a similar hook that apparently doesn't exist anymore now (maybe the docs weren't updated properly): https://mercurius.dev/#/docs/hooks?id=graphql-application-lifecycle-hooks. Check if you can find some info about this to get a sense of how it was implemented before, or perhaps if it's a hook that was moved to the gateway instead
simoneb: @mcollina some guidance here might be useful, especially on the last point of my [comment above](https://github.com/mercurius-js/mercurius/pull/985#issuecomment-1529541622) about the possible async nature of this hook.
mcollina: (CI is failing)
simoneb: > I don’t see a problem in the function becoming asynchronous (minus the wasted promises). That's not a hot path and it would reduce the complexity of the solution.

It would be a breaking change though, as anybody calling extendSchema would now get back a promise to await
simoneb: > (CI is failing)

yep we know, broken while waiting for some direction where to go, as it fails due to missing test coverage
mcollina: Ah now I understand. No, I don't think we should make `extendSchema` async.
simoneb: @stearm given Matteo's feedback above, I'd check if we're missing anything as I mentioned in the this point of my [earlier comment](https://github.com/mercurius-js/mercurius/pull/985#issuecomment-1529541622). Maybe there's a way (or actually, a compromise of some sort) that doesn't require making `extendSchema` async. Let's keep digging for a little longer.
stearm: I see also the [onGatewayReplaceSchemaHandler on mercurius-gateway](https://github.com/mercurius-js/mercurius-gateway/blob/c2f6c6b43926cf41f1002c24881c92b550cf9113/lib/gateway.js#L152) (that is an application hook) is async. I'm unable to see a non-disruptive way to make the onExtendSchema hook syncronous, and then the `extendSchema`. Maybe a breaking change is needed here, what do you think? @simoneb @mcollina 
simoneb: > I see the [onGatewayReplaceSchemaHandler on mercurius-gateway](https://github.com/mercurius-js/mercurius-gateway/blob/c2f6c6b43926cf41f1002c24881c92b550cf9113/lib/gateway.js#L152) (that is an application hook) is also async. I'm unable to see a non-disruptive way to make the `onExtendSchema` hook syncronous, and then the `extendSchema`. Maybe a breaking change is needed here, what do you think? @simoneb @mcollina

I agree that there's no obvious non-breaking way to introduce this change. Since this pattern already exists in the gateway I think it's reasonable to introduce it here as well, but clearly we'd have to wait until the next semver major.
stearm: > > I see the [onGatewayReplaceSchemaHandler on mercurius-gateway](https://github.com/mercurius-js/mercurius-gateway/blob/c2f6c6b43926cf41f1002c24881c92b550cf9113/lib/gateway.js#L152) (that is an application hook) is also async. I'm unable to see a non-disruptive way to make the `onExtendSchema` hook syncronous, and then the `extendSchema`. Maybe a breaking change is needed here, what do you think? @simoneb @mcollina
> 
> I agree that there's no obvious non-breaking way to introduce this change. Since this pattern already exists in the gateway I think it's reasonable to introduce it here as well, but clearly we'd have to wait until the next semver major.

If the implementation is ok, I can proceed and update the docs.
mcollina: Seems to keep failing on Node v20.
simoneb: > Seems to keep failing on Node v20.

We noticed, but it doesn't look like the issue is being caused by changes in this PR. The build on Node 20 has failed in the past also on master, and also because of timeouts. I've had a quick look but I could pinpoint the issue to any specific changes unfortunately, and it's probably unlikely to be caused by recent changes either, but rather Node 20 itself, and for reasons I couldn't figure out.
mcollina: > > Seems to keep failing on Node v20.
> 
> We noticed, but it doesn't look like the issue is being caused by changes in this PR. The build on Node 20 has failed in the past also on master, and also because of timeouts. I've had a quick look but I could pinpoint the issue to any specific changes unfortunately, and it's probably unlikely to be caused by recent changes either, but rather Node 20 itself, and for reasons I couldn't figure out.

The last commit on master passed in https://github.com/mercurius-js/mercurius/actions/runs/4850505716/jobs/8643465117.
stearm: > > The last commit on master passed in https://github.com/mercurius-js/mercurius/actions/runs/4850505716/jobs/8643465117.
> 
> I know, but two before failed for timeout reasons too https://github.com/mercurius-js/mercurius/actions/runs/4818964292/jobs/8581598028

I can add for info that tests are running fine on my machine using node v20 (macOS).
mcollina: Please rebase/merge on master because it fixes the Node v20 issue
stearm: > Please rebase/merge on master because it fixes the Node v20 issue

✅
mcollina: @simoneb are you ok with this or are there further changes?
simoneb: LGTM

Patch:
diff --git a/docs/hooks.md b/docs/hooks.md
index 614e478..c28122e 100644
--- a/docs/hooks.md
+++ b/docs/hooks.md
@@ -200,10 +200,18 @@ fastify.graphql.addHook('preSubscriptionParsing', async (schema, source, context
 
 ## GraphQL Application lifecycle Hooks
 
-There is one hook that you can use in a GraphQL application.
-
 When registering hooks, you must wait for Mercurius to be registered in Fastify.
 
 ```js
 await fastify.ready()
 ```
+
+### onExtendSchema
+
+This hook will be triggered when `extendSchema` is called. It receives the new schema and context object.
+
+```js
+app.graphql.addHook('onExtendSchema', async (schema, context) => {
+  await asyncMethod()
+})
+```
diff --git a/index.d.ts b/index.d.ts
index 0fbcd36..2be6c4d 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -175,6 +175,13 @@ export interface onSubscriptionEndHookHandler<TContext = MercuriusContext> {
 // Application Lifecycle hooks
 // ----------------------------
 
+export interface onExtendSchemaHandler<TContext = MercuriusContext> {
+  (
+    schema: GraphQLSchema,
+    context: TContext,
+  ): Promise<void> | void;
+}
+
 interface ServiceConfig {
   setSchema: (schema: string) => ServiceConfig;
 }
@@ -198,7 +205,7 @@ export interface MercuriusPlugin {
    * Extend existing schema
    * @param schema graphql schema
    */
-  extendSchema(schema: string | Source | DocumentNode): void;
+  extendSchema(schema: string | Source | DocumentNode): Promise<void> | void;
   /**
    * Define additional resolvers
    * @param resolvers object with resolver functions
@@ -280,6 +287,7 @@ export interface MercuriusPlugin {
   addHook<TContext = MercuriusContext>(name: 'onSubscriptionEnd', hook: onSubscriptionEndHookHandler<TContext>): void;
 
   // Application lifecycle addHooks
+  addHook<TContext = MercuriusContext>(name: 'onExtendSchema', hook: onExtendSchemaHandler<TContext>): void;
 }
 
 interface QueryRequest {
diff --git a/index.js b/index.js
index 171b7fa..6ffc200 100644
--- a/index.js
+++ b/index.js
@@ -36,13 +36,14 @@ const {
   MER_ERR_INVALID_OPTS,
   MER_ERR_METHOD_NOT_ALLOWED
 } = require('./lib/errors')
-const { Hooks, assignLifeCycleHooksToContext } = require('./lib/hooks')
+const { Hooks, assignLifeCycleHooksToContext, assignApplicationHooksToContext } = require('./lib/hooks')
 const { kLoaders, kFactory, kSubscriptionFactory, kHooks } = require('./lib/symbols')
 const {
   preParsingHandler,
   preValidationHandler,
   preExecutionHandler,
-  onResolutionHandler
+  onResolutionHandler,
+  onExtendSchemaHandler
 } = require('./lib/handlers')
 
 function buildCache (opts) {
@@ -247,6 +248,12 @@ const plugin = fp(async function (app, opts) {
     }
 
     fastifyGraphQl.schema = extendSchema(fastifyGraphQl.schema, s)
+
+    const context = assignApplicationHooksToContext({}, fastifyGraphQl[kHooks])
+
+    if (context.onExtendSchema !== null) {
+      return onExtendSchemaHandler({ schema: fastifyGraphQl.schema, context })
+    }
   }
 
   fastifyGraphQl.defineResolvers = fastifyGraphQl.defineResolvers || function (resolvers) {
diff --git a/lib/handlers.js b/lib/handlers.js
index 34d9d9a..4eb06cd 100644
--- a/lib/handlers.js
+++ b/lib/handlers.js
@@ -88,6 +88,14 @@ async function onSubscriptionEndHandler (request) {
   )
 }
 
+async function onExtendSchemaHandler (request) {
+  await hooksRunner(
+    request.context.onExtendSchema,
+    hookRunner,
+    request
+  )
+}
+
 module.exports = {
   preParsingHandler,
   preValidationHandler,
@@ -96,5 +104,6 @@ module.exports = {
   preSubscriptionParsingHandler,
   preSubscriptionExecutionHandler,
   onSubscriptionResolutionHandler,
-  onSubscriptionEndHandler
+  onSubscriptionEndHandler,
+  onExtendSchemaHandler
 }
diff --git a/lib/hooks.js b/lib/hooks.js
index cf8274c..3a16b3b 100644
--- a/lib/hooks.js
+++ b/lib/hooks.js
@@ -1,7 +1,9 @@
 'use strict'
 
 const applicationHooks = [
+  'onExtendSchema'
 ]
+
 const lifecycleHooks = [
   'preParsing',
   'preValidation',
@@ -24,6 +26,7 @@ function Hooks () {
   this.preSubscriptionExecution = []
   this.onSubscriptionResolution = []
   this.onSubscriptionEnd = []
+  this.onExtendSchema = []
 }
 
 Hooks.prototype.validate = function (hook, fn) {
@@ -61,6 +64,14 @@ function assignLifeCycleHooksToContext (context, hooks) {
   return Object.assign(context, contextHooks)
 }
 
+function assignApplicationHooksToContext (context, hooks) {
+  const contextHooks = {
+    onExtendSchema: null
+  }
+  if (hooks.onExtendSchema.length > 0) contextHooks.onExtendSchema = hooks.onExtendSchema.slice()
+  return Object.assign(context, contextHooks)
+}
+
 async function hooksRunner (functions, runner, request) {
   for (const fn of functions) {
     await runner(fn, request)
@@ -119,6 +130,7 @@ function onEndHookRunner (fn, request) {
 module.exports = {
   Hooks,
   assignLifeCycleHooksToContext,
+  assignApplicationHooksToContext,
   hooksRunner,
   preExecutionHooksRunner,
   hookRunner,
diff --git a/package.json b/package.json
index 3991b7c..33cd8a3 100644
--- a/package.json
+++ b/package.json
@@ -35,7 +35,7 @@
     "@graphql-tools/utils": "^9.1.4",
     "@sinonjs/fake-timers": "^10.0.2",
     "@types/isomorphic-form-data": "^2.0.0",
-    "@types/node": "^18.0.0",
+    "@types/node": "^20.1.4",
     "@types/ws": "^8.2.0",
     "@typescript-eslint/eslint-plugin": "^5.21.0",
     "@typescript-eslint/parser": "^5.21.0",


Test patch:
diff --git a/test/app-decorator.js b/test/app-decorator.js
index dded089..fad4aeb 100644
--- a/test/app-decorator.js
+++ b/test/app-decorator.js
@@ -246,11 +246,8 @@ test('replaceSchema (clearing cache)', async (t) => {
   )
 
   query = '{ subtract(x: 4, y: 2) }'
-  try {
-    await app.graphql(query)
-  } catch (err) {
-    t.equal(err.errors[0].message, 'Cannot query field "subtract" on type "Query".')
-  }
+
+  await t.rejects(app.graphql(query), { errors: [{ message: 'Cannot query field "subtract" on type "Query".' }] })
 })
 
 test('replaceSchema (without cache)', async (t) => {
@@ -304,11 +301,8 @@ test('replaceSchema (without cache)', async (t) => {
   )
 
   query = '{ subtract(x: 4, y: 2) }'
-  try {
-    await app.graphql(query)
-  } catch (err) {
-    t.equal(err.errors[0].message, 'Cannot query field "subtract" on type "Query".')
-  }
+
+  await t.rejects(app.graphql(query), { errors: [{ message: 'Cannot query field "subtract" on type "Query".' }] })
 })
 
 test('replaceSchema with makeSchemaExecutable (schema should be provided)', async (t) => {
@@ -333,11 +327,7 @@ test('replaceSchema with makeSchemaExecutable (schema should be provided)', asyn
     app.graphql.replaceSchema()
   })
 
-  try {
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Invalid options: Must provide valid Document AST')
-  }
+  await t.rejects(app.ready(), 'Invalid options: Must provide valid Document AST')
 })
 
 test('extendSchema and defineResolvers for query', async (t) => {
@@ -1003,11 +993,7 @@ test('extended Schema is not string', async t => {
     app.graphql.extendSchema(schema2)
   })
 
-  try {
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Invalid options: Must provide valid Document AST')
-  }
+  await t.rejects(app.ready(), { message: 'Invalid options: Must provide valid Document AST' })
 })
 
 test('extended Schema is undefined', async t => {
@@ -1022,11 +1008,7 @@ test('extended Schema is undefined', async t => {
     app.graphql.extendSchema()
   })
 
-  try {
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Invalid options: Must provide valid Document AST')
-  }
+  await t.rejects(app.ready(), { message: 'Invalid options: Must provide valid Document AST' })
 })
 
 test('extended Schema is an object', async t => {
@@ -1095,16 +1077,14 @@ test('Error in schema', async (t) => {
 
   const app = Fastify()
 
-  try {
-    app.register(GQL, {
-      schema,
-      resolvers
-    })
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Interface field Event.Id expected but CustomEvent does not provide it.')
-    t.equal(error.name, 'GraphQLError')
-  }
+  app.register(GQL, {
+    schema,
+    resolvers
+  })
+  await t.rejects(app.ready(), {
+    message: 'Interface field Event.Id expected but CustomEvent does not provide it.',
+    name: 'GraphQLError'
+  })
 })
 
 test('Multiple errors in schema', async (t) => {
@@ -1131,20 +1111,24 @@ test('Multiple errors in schema', async (t) => {
 
   const app = Fastify()
 
-  try {
-    app.register(GQL, {
-      schema,
-      resolvers
-    })
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Invalid schema: check out the .errors property on the Error')
-    t.equal(error.name, 'FastifyError')
-    t.equal(error.errors[0].message, 'Interface field Event.Id expected but CustomEvent does not provide it.')
-    t.equal(error.errors[0].name, 'GraphQLError')
-    t.equal(error.errors[1].message, 'Interface field Event.Id expected but AnotherEvent does not provide it.')
-    t.equal(error.errors[1].name, 'GraphQLError')
-  }
+  app.register(GQL, {
+    schema,
+    resolvers
+  })
+  await t.rejects(app.ready(), {
+    message: 'Invalid schema: check out the .errors property on the Error',
+    name: 'FastifyError',
+    errors: [
+      {
+        message: 'Interface field Event.Id expected but CustomEvent does not provide it.',
+        name: 'GraphQLError'
+      },
+      {
+        message: 'Interface field Event.Id expected but AnotherEvent does not provide it.',
+        name: 'GraphQLError'
+      }
+    ]
+  })
 })
 
 test('defineResolvers should throw if field is not defined in schema', async (t) => {
@@ -1319,12 +1303,10 @@ test('throws on invalid ast input', async (t) => {
       }
     ]
   }`
-  try {
-    await app.graphql(query)
-  } catch (err) {
-    t.equal(err.message, 'Invalid AST Node: { definitions: [[Object]] }.')
-    t.equal(err.name, 'Error')
-  }
+  await t.rejects(app.graphql(query), {
+    message: 'Invalid AST Node: { definitions: [[Object]] }.',
+    name: 'Error'
+  })
 })
 
 test('support ast input on external requests', async (t) => {
diff --git a/test/disable-instrospection.js b/test/disable-instrospection.js
index f603fcc..351820a 100644
--- a/test/disable-instrospection.js
+++ b/test/disable-instrospection.js
@@ -34,13 +34,7 @@ test('should disallow instrospection with "__schema" when NoSchemaIntrospectionC
 
   // needed so that graphql is defined
   await app.ready()
-
-  try {
-    await app.graphql(query)
-  } catch (e) {
-    t.equal(e.errors.length > 0, true)
-    t.equal(e.errors[0].message, 'GraphQL introspection has been disabled, but the requested query contained the field "__schema".')
-  }
+  await t.rejects(app.graphql(query), { errors: [{ message: 'GraphQL introspection has been disabled, but the requested query contained the field "__schema".' }] })
 })
 
 test('should disallow instrospection with "__type" when NoSchemaIntrospectionCustomRule are applied to validationRules', async (t) => {
@@ -57,11 +51,5 @@ test('should disallow instrospection with "__type" when NoSchemaIntrospectionCus
 
   // needed so that graphql is defined
   await app.ready()
-
-  try {
-    await app.graphql(query)
-  } catch (e) {
-    t.equal(e.errors.length > 0, true)
-    t.equal(e.errors[0].message, 'GraphQL introspection has been disabled, but the requested query contained the field "__type".')
-  }
+  await t.rejects(app.graphql(query), { errors: [{ message: 'GraphQL introspection has been disabled, but the requested query contained the field "__type".' }] })
 })
diff --git a/test/hooks.js b/test/hooks.js
index 2b4ffba..b2e67c5 100644
--- a/test/hooks.js
+++ b/test/hooks.js
@@ -1,6 +1,7 @@
 'use strict'
 
 const { test } = require('tap')
+const sinon = require('sinon')
 const Fastify = require('fastify')
 const proxyquire = require('proxyquire')
 const { mapSchema } = require('@graphql-tools/utils')
@@ -126,38 +127,24 @@ test('hooks can be called multiple times', async t => {
 })
 
 test('hooks validation should handle invalid hook names', async t => {
-  t.plan(1)
   const app = await createTestServer(t)
-
-  try {
-    app.graphql.addHook('unsupportedHook', async () => {})
-  } catch (e) {
-    t.equal(e.message, 'unsupportedHook hook not supported!')
-  }
+  t.rejects(async () => app.graphql.addHook('unsupportedHook', async () => {}), { message: 'unsupportedHook hook not supported!' })
 })
 
 test('hooks validation should handle invalid hook name types', async t => {
-  t.plan(2)
   const app = await createTestServer(t)
-
-  try {
-    app.graphql.addHook(1, async () => {})
-  } catch (e) {
-    t.equal(e.code, 'MER_ERR_HOOK_INVALID_TYPE')
-    t.equal(e.message, 'The hook name must be a string')
-  }
+  t.rejects(async () => app.graphql.addHook(1, async () => {}), {
+    code: 'MER_ERR_HOOK_INVALID_TYPE',
+    message: 'The hook name must be a string'
+  })
 })
 
 test('hooks validation should handle invalid hook handlers', async t => {
-  t.plan(2)
   const app = await createTestServer(t)
-
-  try {
-    app.graphql.addHook('preParsing', 'not a function')
-  } catch (e) {
-    t.equal(e.code, 'MER_ERR_HOOK_INVALID_HANDLER')
-    t.equal(e.message, 'The hook callback must be a function')
-  }
+  t.rejects(async () => app.graphql.addHook('preParsing', 'not a function'), {
+    code: 'MER_ERR_HOOK_INVALID_HANDLER',
+    message: 'The hook callback must be a function'
+  })
 })
 
 test('hooks should trigger when JIT is enabled', async t => {
@@ -571,12 +558,12 @@ test('preExecution hooks should be able to modify the schema document AST', asyn
   t.plan(8)
   const app = await createTestServer(t)
 
-  const query = `{ 
-    __type(name:"Query") { 
+  const query = `{
+    __type(name:"Query") {
       name
       fields {
         name
-      }  
+      }
     }
   }`
 
@@ -945,3 +932,42 @@ test('onResolution hooks should be able to add extensions data', async t => {
     }
   })
 })
+
+test('onExtendSchema hooks should be triggered when extendSchema is called', async t => {
+  t.plan(1)
+
+  const app = await createTestServer(t)
+
+  app.graphql.addHook('onExtendSchema', async (schema, context) => {
+    t.pass('onExtendSchema called')
+  })
+
+  const extendedSchema = `
+    extend type Query {
+      sub(x: Int, y: Int): Int
+    }
+  `
+
+  const extendedResolvers = {
+    Query: {
+      sub: async (_, { x, y }) => x - y
+    }
+  }
+
+  await app.register(async function (app) {
+    app.graphql.extendSchema(extendedSchema)
+    app.graphql.defineResolvers(extendedResolvers)
+  })
+})
+
+test('onExtendSchema hooks should not be triggered if extendSchema is not called', async t => {
+  const onExtendSchemaFn = sinon.stub()
+
+  const app = await createTestServer(t)
+
+  app.graphql.addHook('onExtendSchema', async (schema, context) => {
+    onExtendSchemaFn()
+  })
+
+  sinon.assert.notCalled(onExtendSchemaFn)
+})
diff --git a/test/internals/hooksRunner.js b/test/internals/hooksRunner.js
index 25ff470..c6c4065 100644
--- a/test/internals/hooksRunner.js
+++ b/test/internals/hooksRunner.js
@@ -31,11 +31,7 @@ test('hooksRunner - Basic', (t) => {
 test('hooksRunner - In case of error should skip subsequent functions', async (t) => {
   t.plan(3)
 
-  try {
-    await hooksRunner([fn1, fn2, fn3], iterator, 'a')
-  } catch (err) {
-    t.equal(err.message, 'kaboom')
-  }
+  await t.rejects(hooksRunner([fn1, fn2, fn3], iterator, 'a'), { message: 'kaboom' })
 
   function iterator (fn, a) {
     return fn(a)
@@ -118,13 +114,7 @@ test('hooksRunner - Promises that resolve to a value do not change the state', (
 })
 
 test('hooksRunner - Should handle when iterator errors', async (t) => {
-  t.plan(1)
-
-  try {
-    await hooksRunner([fn1, fn2], iterator, 'a')
-  } catch (err) {
-    t.equal(err.message, 'kaboom')
-  }
+  await t.rejects(hooksRunner([fn1, fn2], iterator, 'a'), { message: 'kaboom' })
 
   function iterator (fn) {
     throw new Error('kaboom')
@@ -172,12 +162,7 @@ test('preExecutionHooksRunner - In case of error should skip subsequent function
   t.plan(7)
 
   const originalRequest = { schema: 'schema', document: 'document', context: 'context' }
-
-  try {
-    await preExecutionHooksRunner([fn1, fn2, fn3], originalRequest)
-  } catch (err) {
-    t.equal(err.message, 'kaboom')
-  }
+  await t.rejects(preExecutionHooksRunner([fn1, fn2, fn3], originalRequest), { message: 'kaboom' })
 
   function fn1 (schema, document, context) {
     t.equal(schema, 'schema')
@@ -269,13 +254,8 @@ test('preExecutionHooksRunner - Should handle thrown errors', async t => {
   t.plan(8)
 
   const originalRequest = { schema: 'schema', document: 'document', context: 'context' }
-
-  try {
-    await preExecutionHooksRunner([fn1, fn2, fn3], originalRequest)
-  } catch (err) {
-    t.equal(err.message, 'kaboom')
-    t.same(originalRequest, { schema: 'schema', document: 'document', context: 'context' })
-  }
+  await t.rejects(preExecutionHooksRunner([fn1, fn2, fn3], originalRequest), { message: 'kaboom' })
+  t.same(originalRequest, { schema: 'schema', document: 'document', context: 'context' })
 
   function fn1 (schema, document, context) {
     t.equal(schema, 'schema')
diff --git a/test/loaders.js b/test/loaders.js
index 9c74fce..4e34caf 100644
--- a/test/loaders.js
+++ b/test/loaders.js
@@ -448,13 +448,10 @@ test('rersolver unknown type', async t => {
     resolvers
   })
 
-  try {
-    // needed so that graphql is defined
+  await t.rejects(async () => {
     await app.ready()
     app.graphql('query { test }')
-  } catch (error) {
-    t.equal(error.message, 'Invalid options: Cannot find type test')
-  }
+  }, { message: 'Invalid options: Cannot find type test' })
 })
 
 test('minJit is not a number, throw error', async t => {
@@ -464,11 +461,7 @@ test('minJit is not a number, throw error', async t => {
     jit: '0'
   })
 
-  try {
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Invalid options: the jit option must be a number')
-  }
+  await t.rejects(app.ready(), { message: 'Invalid options: the jit option must be a number' })
 })
 
 test('options cache is type = number', async t => {
@@ -482,21 +475,6 @@ test('options cache is type = number', async t => {
   await app.ready()
 })
 
-test('options cache is boolean', async t => {
-  const app = Fastify()
-
-  app.register(GQL, {
-    cache: true,
-    schema
-  })
-
-  try {
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Invalid options: Cache type is not supported')
-  }
-})
-
 test('options cache is !number && !boolean', async t => {
   const app = Fastify()
 
@@ -504,11 +482,7 @@ test('options cache is !number && !boolean', async t => {
     cache: 'cache'
   })
 
-  try {
-    await app.ready()
-  } catch (error) {
-    t.equal(error.message, 'Invalid options: Cache type is not supported')
-  }
+  await t.rejects(app.ready(), { message: 'Invalid options: Cache type is not supported' })
 })
 
 test('options cache is false and lruErrors exists', async t => {
@@ -521,13 +495,7 @@ test('options cache is false and lruErrors exists', async t => {
 
   // needed so that graphql is defined
   await app.ready()
-
-  try {
-    await app.graphql('{ dogs { name { owner } } }')
-  } catch (error) {
-    t.equal(error.message, 'Graphql validation error')
-    t.end()
-  }
+  await t.rejects(app.graphql('{ dogs { name { owner } } }'), { message: 'Graphql validation error' })
 })
 
 test('reply is empty, throw error', async (t) => {
diff --git a/test/query-depth.js b/test/query-depth.js
index 40d3cb0..37dbe19 100644
--- a/test/query-depth.js
+++ b/test/query-depth.js
@@ -156,7 +156,6 @@ test('queryDepth - test total depth is within queryDepth parameter', async (t) =
 })
 
 test('queryDepth - test total depth is over queryDepth parameter', async (t) => {
-  t.plan(1)
   const app = Fastify()
 
   app.register(GQL, {
@@ -172,11 +171,7 @@ test('queryDepth - test total depth is over queryDepth parameter', async (t) =>
   const queryDepthError = new MER_ERR_GQL_QUERY_DEPTH('unnamedQuery', 6, 5)
   err.errors = [queryDepthError]
 
-  try {
-    await app.graphql(query)
-  } catch (error) {
-    t.same(error, err)
-  }
+  await t.rejects(app.graphql(query), err)
 })
 
 test('queryDepth - queryDepth is not number', async (t) => {
diff --git a/test/subscriber.js b/test/subscriber.js
index b70e9b6..661752a 100644
--- a/test/subscriber.js
+++ b/test/subscriber.js
@@ -67,7 +67,6 @@ test('subscription context publish event errs, error is catched', t => {
 })
 
 test('subscription context publish event returns a promise reject on error', async t => {
-  t.plan(1)
   const emitter = mq()
   const error = new Error('Dummy error')
   emitter.on = (topic, listener, done) => done(error)
@@ -75,11 +74,7 @@ test('subscription context publish event returns a promise reject on error', asy
   const pubsub = new PubSub(emitter)
   const sc = new SubscriptionContext({ pubsub })
 
-  try {
-    await sc.subscribe('TOPIC')
-  } catch (e) {
-    t.same(error, e)
-  }
+  await t.rejects(sc.subscribe('TOPIC'), error)
 })
 
 test('subscription context can handle multiple topics', t => {
diff --git a/test/validation-rules.js b/test/validation-rules.js
index 2014816..f64f299 100644
--- a/test/validation-rules.js
+++ b/test/validation-rules.js
@@ -23,7 +23,6 @@ const resolvers = {
 const query = '{ add(x: 2, y: 2) }'
 
 test('validationRules array - reports an error', async (t) => {
-  t.plan(2)
   const app = Fastify()
 
   app.register(GQL, {
@@ -43,13 +42,7 @@ test('validationRules array - reports an error', async (t) => {
 
   // needed so that graphql is defined
   await app.ready()
-
-  try {
-    await app.graphql(query)
-  } catch (e) {
-    t.equal(e.errors.length, 1)
-    t.equal(e.errors[0].message, 'Validation rule error')
-  }
+  await t.rejects(app.graphql(query), { errors: [{ message: 'Validation rule error' }] })
 })
 
 test('validationRules array - passes when no errors', async (t) => {
@@ -96,7 +89,6 @@ test('validationRules array - works with empty validationRules', async (t) => {
 })
 
 test('validationRules - reports an error', async (t) => {
-  t.plan(2)
   const app = Fastify()
 
   app.register(GQL, {
@@ -117,13 +109,7 @@ test('validationRules - reports an error', async (t) => {
 
   // needed so that graphql is defined
   await app.ready()
-
-  try {
-    await app.graphql(query)
-  } catch (e) {
-    t.equal(e.errors.length, 1)
-    t.equal(e.errors[0].message, 'Validation rule error')
-  }
+  await t.rejects(app.graphql(query), { errors: [{ message: 'Validation rule error' }] })
 })
 
 test('validationRules - passes when no errors', async (t) => {
@@ -227,7 +213,6 @@ test('validationRules - includes graphql request metadata', async (t) => {
 })
 
 test('validationRules - errors if cache is used with the function', async (t) => {
-  t.plan(1)
   const app = Fastify()
 
   app.register(GQL, {
@@ -238,10 +223,5 @@ test('validationRules - errors if cache is used with the function', async (t) =>
   })
 
   // needed so that graphql is defined
-
-  try {
-    await app.ready()
-  } catch (e) {
-    t.equal(e.message, 'Invalid options: Using a function for the validationRules is incompatible with query caching')
-  }
+  await t.rejects(app.ready(), { message: 'Invalid options: Using a function for the validationRules is incompatible with query caching' })
 })