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:
-
GraphQL for VS Code
- Syntax highlighting
- Schema validation
- Autocompletion
- Go to definition
-
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.