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
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
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
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.
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
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
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')
}
}