Testing

GraphQL Yoga makes it easy to test your GraphQL API. It has built-in support for HTTP injection. You can use any testing framework of your choice.

You can use the fetch method on your yoga instance for calling the yoga instance as if you were doing an HTTP request.

💡

Calling the yoga.fetch method does not send an actual HTTP request. It simulates the HTTP request which 100% conforms with how Request/Response work.

Test Utility

Writing parsers for the Subscription or Incremental Delivery (@defer/@stream) protocol is annoying and cumbersome. Instead, you can use the buildHTTPExecutor utility function from @graphql-tools/executor-http.

npm i @graphql-tools/executor-http
Building an executor
import { createSchema, createYoga } from 'graphql-yoga'
import { buildHTTPExecutor } from '@graphql-tools/executor-http'
 
const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      greetings: String!
    }
  `,
  resolvers: {
    Query: {
      greetings: () => 'Hello World!'
    }
  }
})
 
const yoga = createYoga({ schema })
 
const executor = buildHTTPExecutor({
  fetch: yoga.fetch
})

Mutation and Query Operations

Mutation and Query Operations
import { parse } from 'graphql'
import { createSchema, createYoga } from 'graphql-yoga'
import { buildHTTPExecutor } from '@graphql-tools/executor-http'
 
const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      greetings: String!
    }
  `,
  resolvers: {
    Query: {
      greetings: () => 'Hello World!'
    }
  }
})
 
function assertSingleValue<TValue extends object>(
  value: TValue | AsyncIterable<TValue>
): asserts value is TValue {
  if (Symbol.asyncIterator in value) {
    throw new Error('Expected single value')
  }
}
 
const yoga = createYoga({ schema })
 
const executor = buildHTTPExecutor({
  fetch: yoga.fetch,
  endpoint: `http://yoga/graphql`
})
 
const result = await executor({
  document: parse(/* GraphQL */ `
    query {
      greetings
    }
  `)
})
 
assertSingleValue(result)
 
console.assert(
  result.data?.greetings === 'Hello World!',
  `Expected 'Hello World!' but got ${result.data.greetings}`
)

Subscription Operations

Subscription Operations
import { parse } from 'graphql'
import { createSchema, createYoga, Repeater } from 'graphql-yoga'
import { buildHTTPExecutor } from '@graphql-tools/executor-http'
 
const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      greetings: String!
    }
 
    type Subscription {
      counter: Int!
    }
  `,
  resolvers: {
    Subscription: {
      counter: {
        subscribe: () =>
          new Repeater((push, end) => {
            let i = 0
            const interval = setInterval(() => {
              push({ counter: i++ })
            }, 100)
 
            end.then(() => clearInterval(interval))
          })
      }
    }
  }
})
 
function assertStreamValue<TValue extends object>(
  value: TValue | AsyncIterable<TValue>
): asserts value is AsyncIterable<TValue> {
  if (Symbol.asyncIterator in value) {
    return
  }
  throw new Error('Expected single value')
}
 
const executor = buildHTTPExecutor({
  fetch: yoga.fetch
})
 
const result1 = await executor({
  document: parse(/* GraphQL */ `
    subscription {
      counter
    }
  `)
})
 
const result2 = await executor({
  document: parse(/* GraphQL */ `
    subscription {
      counter
    }
  `)
})
 
assertStreamValue(result1)
assertStreamValue(result2)
 
let iterationCounter = 0
 
for await (const value of result1) {
  if (iterationCounter === 0) {
    console.assert(value.data?.counter === 1, `Expected 1 but got ${value.data?.counter}`)
    iterationCounter++
  } else if (iterationCounter === 1) {
    console.assert(value.data?.counter === 1, `Expected 2 but got ${value.data?.counter}`)
    break
  } else {
    throw new Error('Expected only two iterations')
  }
}
// the server will terminate the iteration after yielding 2 values, for this subscription resolver definition
 
// on the other hand, for most use cases where we are listening to an indefinitely long stream, we trigger cleanup from the client-side as follows:
// here we demonstrate listening to one value:
const result2It = result2[Symbol.asyncIterator]()
const itResult = await result2It.next()
console.assert(itResult.value.data?.counter === 1)
// after listening to the value, we cleanup by calling return(), if present
await roomUpdatesIterator.return?.()

TypeScript safe Test Suits with GraphQL Code Generator

The GraphQL Code Generator allows you to generate TypeScript types for your GraphQL operations (queries, mutations and subscriptions), leading to a better developer experience when writing tests.

You can simply pass the documents from GraphQL Code Generator to the executor for a fully typed experience.

TypeScript safe usage of executor
import { buildHTTPExecutor } from '@graphql-tools/executor-http'
import { graphql } from './gql'
import { yoga } from './yoga'
 
const HelloWorldQuery = graphql(/* GraphQL */ `
  query HelloWorld {
    hello
  }
`)
 
const executor = buildHTTPExecutor({
  fetch: yoga.fetch
})
 
const result = await executor({
  document: HelloWorldQuery
})
 
// this is now a type-safe when being accessed
// result.data?.hello

Learn more on the GraphQL Code Generator API Testing Guide

Advanced

Instead of using the convinient @graphql-tools/executor-http package you can also process the request body from scratch.

Mutation and Query Operation

Query Operation Test
import { createSchema, createYoga } from 'graphql-yoga'
 
const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      greetings: String!
    }
  `,
  resolvers: {
    Query: {
      greetings: () => 'Hello World!'
    }
  }
})
 
const yoga = createYoga({ schema })
 
const response = await yoga.fetch('http://yoga/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    query: '{ greetings }'
  })
})
 
console.assert(response.status === 200, 'Response status should be 200')
const executionResult = await response.json()
console.assert(
  executionResult.data?.greetings === 'Hello World!',
  `Expected 'Hello World!' but got ${executionResult.data?.greetings}`
)

Subscription Operation

Query Operation Test
import { createSchema, createYoga, Repeater } from 'graphql-yoga'
 
const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Query {
      greetings: String!
    }
 
    type Subscription {
      counter: Int!
    }
  `,
  resolvers: {
    Subscription: {
      counter: {
        subscribe: () =>
          new Repeater((push, end) => {
            let i = 0
            const interval = setInterval(() => {
              push({ counter: i++ })
            }, 100)
 
            end.then(() => clearInterval(interval))
          })
      }
    }
  }
})
 
const yoga = createYoga({ schema })
 
function eventStream<TType = unknown>(source: ReadableStream<Uint8Array>) {
  return new Repeater<TType>(async (push, end) => {
    const cancel: Promise<{ done: true }> = end.then(() => ({ done: true }))
    const iterable = source[Symbol.asyncIterator]()
    // eslint-disable-next-line no-constant-condition
    while (true) {
      const result = await Promise.race([cancel, iterable.next()])
 
      if (result.done) {
        break
      }
 
      const values = result.value.toString().split('\n\n').filter(Boolean)
      for (const value of values) {
        if (!value.startsWith('data: ')) {
          continue
        }
        const result = value.replace('data: ', '')
        push(JSON.parse(result))
      }
    }
 
    iterable.return?.()
    end()
  })
}
 
const response = await yoga.fetch('http://yoga/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    query: 'subscription { counter }'
  })
})
 
console.assert(response.status === 200, 'Response status should be 200')
let iterationCounter = 0
 
for await (const executionResult of eventStream(response.body!)) {
  if (iterationCounter === 0) {
    console.assert(
      executionResult.data?.counter === 1,
      `Expected 1 but got ${executionResult.data?.counter}`
    )
    iterationCounter++
  } else if (iterationCounter === 1) {
    console.assert(
      executionResult.data?.counter === 2,
      `Expected 2 but got ${executionResult.data?.counter}`
    )
    iterationCounter++
  } else if (iterationCounter === 2) {
    console.assert(
      executionResult.data?.counter === 3,
      `Expected 3 but got ${executionResult.data?.counter}`
    )
    break
  } else {
    throw new Error('Expected only three iterations')
  }
}