Skip to main content

Chapter 9: Tooling and Testing

In this chapter, we'll explore the essential tools and testing strategies for building robust GraphQL APIs with GraphQL Yoga.

Development Tools

GraphQL Playground

GraphQL Yoga comes with a built-in GraphQL Playground that provides an interactive environment for testing your API:

import { createYoga } from '@graphql-yoga/node'

const yoga = createYoga({
schema,
// Enable GraphiQL in development
graphiql: process.env.NODE_ENV !== 'production'
})

Features of the GraphQL Playground:

  • Interactive documentation
  • Query validation
  • Variable support
  • Headers configuration
  • Query history

VS Code Extensions

Recommended VS Code extensions for GraphQL development:

  1. GraphQL for VS Code

    • Syntax highlighting
    • Schema validation
    • Autocompletion
    • Go to definition
  2. Apollo GraphQL

    • Schema validation
    • Query validation
    • Type generation

TypeScript Integration

Leverage TypeScript for type safety:

// types.ts
import { GraphQLResolveInfo } from 'graphql'
import { PrismaClient } from '@prisma/client'

export interface Context {
prisma: PrismaClient
user: User | null
}

export type ResolverFn<TResult, TParent, TContext, TArgs> = (
parent: TParent,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult

// resolvers.ts
import { ResolverFn } from './types'

const resolvers = {
Query: {
user: (async (_, { id }, context) => {
return context.prisma.user.findUnique({
where: { id }
})
}) as ResolverFn<User, {}, Context, { id: string }>
}
}

Testing Strategies

Unit Testing

Test individual resolvers:

// user.test.ts
import { createTestClient } from 'apollo-server-testing'
import { schema } from '../schema'
import { mockPrisma } from '../test/mocks'

describe('User Resolvers', () => {
const { query, mutate } = createTestClient({
schema,
context: {
prisma: mockPrisma
}
})

describe('queries', () => {
it('fetches user by id', async () => {
const res = await query({
query: gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '1' }
})

expect(res.data.user).toEqual({
id: '1',
name: 'John Doe',
email: 'john@example.com'
})
})
})

describe('mutations', () => {
it('creates a new user', async () => {
const res = await mutate({
mutation: gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`,
variables: {
input: {
name: 'Jane Doe',
email: 'jane@example.com'
}
}
})

expect(res.data.createUser).toEqual({
id: '2',
name: 'Jane Doe',
email: 'jane@example.com'
})
})
})
})

Integration Testing

Test the entire API:

// api.test.ts
import { createYoga } from '@graphql-yoga/node'
import { schema } from '../schema'
import { createTestClient } from 'apollo-server-testing'

describe('GraphQL API', () => {
const yoga = createYoga({ schema })
const { query, mutate } = createTestClient(yoga)

it('handles authentication', async () => {
const res = await query({
query: gql`
query GetMe {
me {
id
name
}
}
`,
context: {
headers: {
authorization: 'Bearer invalid-token'
}
}
})

expect(res.errors).toBeDefined()
expect(res.errors[0].message).toBe('Not authenticated')
})

it('handles pagination', async () => {
const res = await query({
query: gql`
query GetUsers($first: Int!, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
name
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
`,
variables: {
first: 2
}
})

expect(res.data.users.edges).toHaveLength(2)
expect(res.data.users.pageInfo.hasNextPage).toBe(true)
})
})

End-to-End Testing

Test the complete application flow:

// e2e.test.ts
import { createYoga } from '@graphql-yoga/node'
import { schema } from '../schema'
import { PrismaClient } from '@prisma/client'

describe('E2E Tests', () => {
let prisma: PrismaClient
let yoga: ReturnType<typeof createYoga>

beforeAll(async () => {
prisma = new PrismaClient()
yoga = createYoga({
schema,
context: {
prisma
}
})
})

afterAll(async () => {
await prisma.$disconnect()
})

it('completes user registration flow', async () => {
// 1. Register user
const registerRes = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
query: `
mutation Register($input: RegisterInput!) {
register(input: $input) {
token
user {
id
name
}
}
}
`,
variables: {
input: {
name: 'Test User',
email: 'test@example.com',
password: 'password123'
}
}
})
})

const { token } = (await registerRes.json()).data.register

// 2. Query protected data
const meRes = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
query: `
query GetMe {
me {
id
name
email
}
}
`
})
})

const { me } = (await meRes.json()).data
expect(me.name).toBe('Test User')
})
})

Test Utilities

Mocking

Create reusable mocks:

// test/mocks.ts
import { PrismaClient } from '@prisma/client'

export const mockPrisma = {
user: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn()
},
post: {
findUnique: jest.fn(),
findMany: jest.fn(),
create: jest.fn(),
update: jest.fn(),
delete: jest.fn()
}
} as unknown as PrismaClient

// test/setup.ts
beforeEach(() => {
jest.clearAllMocks()
})

// Example usage
mockPrisma.user.findUnique.mockResolvedValue({
id: '1',
name: 'John Doe',
email: 'john@example.com'
})

Test Helpers

Create helper functions for testing:

// test/helpers.ts
import { createYoga } from '@graphql-yoga/node'
import { schema } from '../schema'
import { mockPrisma } from './mocks'

export function createTestContext() {
return {
prisma: mockPrisma,
yoga: createYoga({
schema,
context: {
prisma: mockPrisma
}
})
}
}

export async function executeOperation(operation: any, context: any) {
const { yoga } = context

const response = await yoga.fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(operation)
})

return response.json()
}

export function createAuthenticatedContext(user: any) {
const context = createTestContext()

return {
...context,
headers: {
'Authorization': `Bearer ${user.token}`
}
}
}

Performance Testing

Load Testing

Use tools like k6 for load testing:

// load-test.js
import http from 'k6/http'
import { check, sleep } from 'k6'

export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '1m', target: 20 },
{ duration: '30s', target: 0 }
]
}

export default function() {
const res = http.post('http://localhost:4000/graphql', JSON.stringify({
query: `
query GetUsers {
users(first: 10) {
edges {
node {
id
name
posts {
id
title
}
}
}
}
}
`
}), {
headers: {
'Content-Type': 'application/json'
}
})

check(res, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200
})

sleep(1)
}

Performance Monitoring

Monitor API performance:

// server.ts
import { createYoga } from '@graphql-yoga/node'
import { useResponseCache } from '@envelop/response-cache'
import { usePrometheus } from '@envelop/prometheus'

const yoga = createYoga({
schema,
plugins: [
useResponseCache({
ttl: 60 * 1000
}),
usePrometheus({
// Expose metrics at /metrics
path: '/metrics',
// Custom metrics
metrics: {
queryDuration: {
type: 'histogram',
help: 'Duration of GraphQL queries'
}
}
})
]
})

Summary

In this chapter, we've covered:

  • Development tools for GraphQL
  • Testing strategies (unit, integration, e2e)
  • Test utilities and mocking
  • Performance testing and monitoring

These tools and practices will help you build and maintain high-quality GraphQL APIs. In the next chapter, we'll explore deployment strategies.