Skip to content

mercurius-js/mercurius-integration-testing

Repository files navigation

mercurius-integration-testing

npm version codecov

pnpm add mercurius-integration-testing
# or
yarn add mercurius-integration-testing
# or
npm install mercurius-integration-testing

Features

Table of Contents

Usage

// app.ts | app.js
import Fastify from 'fastify'
import Mercurius from 'mercurius'
import schema from './schema'
import { buildContext } from './buildContext'

export const app = Fastify()

app.register(Mercurius, {
  schema,
  resolvers: {},
  context: buildContext,
  allowBatchedQueries: true,
})
// integration.test.js | integration.test.ts

import { createMercuriusTestClient } from 'mercurius-integration-testing'
import { app } from '../app'

// ...

const testClient = createMercuriusTestClient(app)

expect(testClient.query('query { helloWorld }')).resolves.toEqual({
  data: {
    helloWorld: 'helloWorld',
  },
})

API

createMercuriusTestClient

Create a testing client instance, you should give it the fastify instance in which Mercurius was already registered, and optionally, some options

const client = createMercuriusTestClient(app, {
  /**
   * Optional, specify headers to be added to every request in the client
   */
  headers: {
    authorization: 'hello-world',
  },
  /**
   * Optional, by default it points to /graphql
   */
  url: '/graphql',
  /**
   * Optional, specify cookies to be added to every request in the client
   */
  cookies: {
    authorization: 'hello-world',
  },
})

query, mutate

.query and .mutate are basically the same function, but for readability, both exists

// You can give it a simple string
const queryResponse = await client.query(`
query {
  helloWorld
}
`)

// Data returned from the API
queryResponse.data

// Possible array of errors from the API
queryResponse.errors

// You can also call `mutate`
// to improve readability for mutations
const mutationResponse = await client.mutate(`
mutation {
  helloWorld
}
`)
DocumentNode support
// You can also give them `DocumentNode`s
// from `graphql-tag` or equivalents
await client.query(gql`
  query {
    helloWorld
  }
`)
Variables
// You can give variables in the second parameter options
await client.query(
  `
  query($foo: String!) {
    hello(foo: $foo)
  }
`,
  {
    variables: {
      foo: 'bar',
    },
  }
)
Other options
await client.query(
  `
  query example {
    helloExample
  }
`,
  {
    // You can specify operation name if the queries
    // are named
    operationName: 'helloExample',
    // Query specific headers
    // These are going to be "merged" with the client set headers
    headers: {
      hello: 'world',
    },

    // Query specific cookies
    // These are going to be "merged" with the client set headers
    cookies: {
      foo: 'bar',
    },
  }
)

setHeaders

You can change the default client headers whenever

client.setHeaders({
  authorization: 'other-header',
})

setCookies

You can change the default client cookies whenever

client.setCookies({
  authorization: 'other-cookie',
})

batchQueries

If allowBatchedQueries is set in the Mercurius registration, you can call some queries together

const batchedResponse = await client.batchQueries(
  [
    {
      query: `
  query {
    helloWorld
  }
  `,
    },
    {
      query: `
  query($name: String!) {
    user(name: $name) {
      email
    }
  }
  `,
      variables: {
        name: 'bob',
      },
      // operationName: "you-can-specify-it-here-if-needed"
    },
  ],
  // Optional
  {
    // Optional request specific cookies
    cookies: {
      foo: 'bar',
    },
    // Optional request specific headers
    headers: {
      foo: 'bar',
    },
  }
)

batchedResponse ===
  [
    { data: { helloWorld: 'foo' } },
    { data: { user: { email: '[email protected]' } } },
  ]

subscribe

If you are not already calling .listen(PORT) somewhere, it will automatically call it, assigning a random available port, this means you will have to manually call .close() somewhere

.subscribe returns a promise that resolves when the subscription connection is made

headers & cookies are applied the same as in .query and .mutate

const subscription = await client.subscribe({
  query: `
  subscription {
    notificationAdded {
      id
      message
    }
  }
  `,
  onData(response) {
    // response.errors => array of graphql errors or undefined
    response ==
      { data: { notificationAdded: { id: 1, message: 'hello world' } } }
  },
  // Optional
  variables: { foo: 'bar' },
  // Optional
  operationName: 'name_if_is_named_query',
  // Optional, initialization payload, usually for authorization
  initPayload: { authorization: '<token>' },
  // Optional, subscription specific cookies
  cookies: {
    authorization: '<token>',
  },
  // Optional, subscription specific headers
  headers: {
    authorization: '<token>',
  },
})

// You can manually call the unsubscribe

subscription.unsubscribe()

// You will need to manually close the fastify instance somewhere

app.close()

getFederatedEntity

In a federated service it's useful to test if a service is extending the entity correctly.

This function is a wrapper around _entities query.

An entity can be federated on a single field:

import { mercuriusFederationPlugin } from '@mercuriusjs/federation'

const schema = `
    type Post @key(fields: "id") {
      id: ID! @external
      description: String!
    }
  
    extend type User @key(fields: "id") {
      id: ID! @external
      posts: [Post!]!
    }
  `

const app = fastify()
app.register(mercuriusFederationPlugin, {
  schema,
  resolvers: {
    User: {
      posts: () => [{ id: 'post-id', description: 'Post description' }],
    },
  },
})

const client = createMercuriusTestClient(app)

const entity = await client.getFederatedEntity({
  typename: 'User',
  keys: { id: 'user1' },
  typeQuery: `
        id
        posts {
          id
          description
        }`,
})

entity ===
  {
    __typename: 'User',
    id: 'user1',
    posts: [
      {
        id: 'post-id',
        description: 'Post description',
      },
    ],
  }

or on multiple fields:

const schema = `
      type ProductCategory {
        id: ID!
        name: String!
      }
    
      extend type Product @key(fields: "sku") @key(fields: "upc") {
        upc: String! @external
        sku: Int! @external
        category: ProductCategory
      }
    `

const app = fastify()
app.register(mercuriusFederationPlugin, {
  schema,
  resolvers: {
    Product: {
      category: () => ({ id: 'product-category', name: 'Stub category' }),
    },
  },
})

const client = createMercuriusTestClient(app)

const entity = await client.getFederatedEntity({
  typename: 'Product',
  keys: { sku: 1, upc: 'upc' },
  typeQuery: `
          upc
          sku
          category {
            id
            name
          }`,
})

entity ===
  {
    __typename: 'Product',
    upc: 'upc',
    sku: 1,
    category: {
      id: 'product-category',
      name: 'Stub category',
    },
  }

TypeScript

const dataResponse = await client.query<{
  helloWorld: string
}>(`
query {
  helloWorld
}
`)

// string
dataResponse.data.helloWorld

const variablesResponse = await client.query<
  {
    user: {
      email: string
    }
  },
  {
    name: string
  }
>(
  `
  query($name: String!) {
    user(name: $name) {
      email
    }
  }
`,
  {
    variables: {
      name: 'bob',
    },
  }
)

// string
variablesResponse.data.user.email

await client.subscribe<
  {
    helloWorld: string
  },
  {
    foo: string
  }
>({
  query: `
  subscription($foo: String!) {
    helloWorld(foo: $foo)
  }
  `,
  variables: {
    // Error, Type 'number' is not assignable to type 'string'.
    foo: 123,
  },
  onData(response) {
    // string
    response.data.helloWorld
  },
})

License

MIT