Skip to content

Testing

This guide covers comprehensive testing strategies for GQLB applications, including unit tests, integration tests, and performance testing.

Unit Testing Queries

Testing Query Structure

Test that your queries are structured correctly:

typescript
import { describe, it, expect } from 'vitest'
import { b } from '../generated'

describe('User Queries', () => {
  it('should build correct GetUser query', () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [
        b.id(),
        b.name(),
        b.email(),
      ])
    ])

    // Test the generated GraphQL document
    const document = USER_QUERY.document()
    expect(document.kind).toBe('Document')
    expect(document.definitions).toHaveLength(1)
    
    const operation = document.definitions[0]
    expect(operation.kind).toBe('OperationDefinition')
    expect(operation.operation).toBe('query')
    expect(operation.name?.value).toBe('GetUser')
  })

  it('should include correct variables', () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [b.id(), b.name()])
    ])

    const variables = USER_QUERY.variableDefinitions()
    expect(variables).toEqual({
      id: 'ID!'
    })
  })

  it('should generate correct field selection', () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [
        b.id(),
        b.name(),
        b.email(),
      ])
    ])

    const queryString = USER_QUERY.toString()
    expect(queryString).toContain('user(id: $id)')
    expect(queryString).toContain('id')
    expect(queryString).toContain('name')
    expect(queryString).toContain('email')
  })
})

Testing Type Safety

Verify that your queries provide correct TypeScript types:

typescript
import { OutputOf, VariablesOf } from '../generated'

describe('Query Types', () => {
  it('should provide correct output types', () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [
        b.id(),
        b.name(),
        b.email(),
      ])
    ])

    type UserData = OutputOf<typeof USER_QUERY>
    
    // Type assertions to verify structure
    const mockData: UserData = {
      __typename: 'Query',
      user: {
        __typename: 'User',
        id: '123',
        name: 'John Doe',
        email: 'john@example.com'
      }
    }

    expect(mockData.user?.id).toBe('123')
    expect(mockData.user?.name).toBe('John Doe')
  })

  it('should provide correct variable types', () => {
    const USER_QUERY = b.query('GetUser', { 
      id: 'ID!',
      includeEmail: 'Boolean!'
    }, (b, v) => [
      b.user({ id: v.id }, b => [
        b.id(),
        b.name(),
      ])
    ])

    type Variables = VariablesOf<typeof USER_QUERY>
    
    const variables: Variables = {
      id: '123',
      includeEmail: true
    }

    expect(variables.id).toBe('123')
    expect(variables.includeEmail).toBe(true)
  })
})

Testing Fragments

Fragment Reusability Tests

typescript
describe('Fragments', () => {
  const USER_INFO_FRAGMENT = b.fragment('UserInfo', 'User', (b) => [
    b.id(),
    b.name(),
    b.avatar(),
  ])

  it('should be reusable across queries', () => {
    const QUERY_1 = b.query('GetCurrentUser', (b) => [
      b.currentUser(b => [
        ...USER_INFO_FRAGMENT,
        b.email(),
      ])
    ])

    const QUERY_2 = b.query('GetUsers', (b) => [
      b.users(b => [
        ...USER_INFO_FRAGMENT,
        b.bio(),
      ])
    ])

    // Both queries should contain the same fragment fields
    const query1String = QUERY_1.toString()
    const query2String = QUERY_2.toString()

    expect(query1String).toContain('id')
    expect(query1String).toContain('name')
    expect(query1String).toContain('avatar')

    expect(query2String).toContain('id')
    expect(query2String).toContain('name')
    expect(query2String).toContain('avatar')
  })

  it('should maintain type safety in fragments', () => {
    type FragmentData = SelectionSetOutput<typeof USER_INFO_FRAGMENT>
    
    const mockFragment: FragmentData = {
      __typename: 'User',
      id: '123',
      name: 'John Doe',
      avatar: 'https://example.com/avatar.jpg'
    }

    expect(mockFragment.id).toBe('123')
    expect(mockFragment.name).toBe('John Doe')
  })
})

Integration Testing

Testing with Mock GraphQL Server

Set up a mock GraphQL server for integration tests:

typescript
import { graphql, buildSchema } from 'graphql'
import { describe, it, expect, beforeAll } from 'vitest'

const schema = buildSchema(`
  type Query {
    user(id: ID!): User
    posts: [Post!]!
  }
  
  type User {
    id: ID!
    name: String!
    email: String
    posts: [Post!]!
  }
  
  type Post {
    id: ID!
    title: String!
    author: User!
  }
`)

const mockData = {
  users: [
    { id: '1', name: 'John Doe', email: 'john@example.com' },
    { id: '2', name: 'Jane Smith', email: 'jane@example.com' }
  ],
  posts: [
    { id: '1', title: 'Hello World', authorId: '1' },
    { id: '2', title: 'GraphQL Basics', authorId: '2' }
  ]
}

const rootValue = {
  user: ({ id }) => mockData.users.find(u => u.id === id),
  posts: () => mockData.posts.map(post => ({
    ...post,
    author: mockData.users.find(u => u.id === post.authorId)
  }))
}

describe('Query Integration Tests', () => {
  it('should execute user query successfully', async () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [
        b.id(),
        b.name(),
        b.email(),
      ])
    ])

    const result = await graphql({
      schema,
      source: USER_QUERY.toString(),
      variableValues: { id: '1' },
      rootValue
    })

    expect(result.errors).toBeUndefined()
    expect(result.data?.user).toEqual({
      __typename: 'User',
      id: '1',
      name: 'John Doe',
      email: 'john@example.com'
    })
  })

  it('should handle nested queries', async () => {
    const POSTS_WITH_AUTHORS = b.query('GetPostsWithAuthors', (b) => [
      b.posts(b => [
        b.id(),
        b.title(),
        b.author(b => [
          b.id(),
          b.name(),
        ])
      ])
    ])

    const result = await graphql({
      schema,
      source: POSTS_WITH_AUTHORS.toString(),
      rootValue
    })

    expect(result.errors).toBeUndefined()
    expect(result.data?.posts).toHaveLength(2)
    expect(result.data?.posts[0].author.name).toBe('John Doe')
  })
})

Testing with Real GraphQL Client

Test with actual GraphQL client integration:

typescript
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { SchemaLink } from '@apollo/client/link/schema'

describe('Client Integration Tests', () => {
  let client: ApolloClient<any>

  beforeAll(() => {
    client = new ApolloClient({
      link: new SchemaLink({ schema, rootValue }),
      cache: new InMemoryCache()
    })
  })

  it('should work with Apollo Client', async () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [
        b.id(),
        b.name(),
        b.email(),
      ])
    ])

    const result = await client.query({
      query: USER_QUERY.document(),
      variables: { id: '1' }
    })

    expect(result.data.user.name).toBe('John Doe')
    expect(result.loading).toBe(false)
    expect(result.error).toBeUndefined()
  })
})

Testing Mutations

Mutation Testing

typescript
describe('Mutation Tests', () => {
  it('should build create user mutation correctly', () => {
    const CREATE_USER = b.mutation('CreateUser', {
      input: 'CreateUserInput!'
    }, (b, v) => [
      b.createUser({ input: v.input }, b => [
        b.user(b => [
          b.id(),
          b.name(),
          b.email(),
        ]),
        b.errors(b => [
          b.field(),
          b.message(),
        ])
      ])
    ])

    const mutationString = CREATE_USER.toString()
    expect(mutationString).toContain('mutation CreateUser')
    expect(mutationString).toContain('createUser(input: $input)')
    expect(mutationString).toContain('errors')
  })

  it('should handle mutation errors correctly', async () => {
    const mockResolver = {
      createUser: ({ input }) => ({
        user: null,
        errors: [
          { field: 'email', message: 'Email already exists' }
        ]
      })
    }

    const CREATE_USER = b.mutation('CreateUser', {
      input: 'CreateUserInput!'
    }, (b, v) => [
      b.createUser({ input: v.input }, b => [
        b.user(b => [b.id(), b.name()]),
        b.errors(b => [b.field(), b.message()])
      ])
    ])

    const result = await graphql({
      schema,
      source: CREATE_USER.toString(),
      variableValues: { 
        input: { name: 'Test', email: 'existing@example.com' }
      },
      rootValue: mockResolver
    })

    expect(result.data?.createUser.user).toBeNull()
    expect(result.data?.createUser.errors).toHaveLength(1)
    expect(result.data?.createUser.errors[0].field).toBe('email')
  })
})

Testing Subscriptions

Subscription Testing

typescript
import { subscribe } from 'graphql'

describe('Subscription Tests', () => {
  it('should build subscription correctly', () => {
    const MESSAGE_SUBSCRIPTION = b.subscription('NewMessage', {
      channelId: 'ID!'
    }, (b, v) => [
      b.messageAdded({ channelId: v.channelId }, b => [
        b.id(),
        b.content(),
        b.author(b => [
          b.name(),
        ])
      ])
    ])

    const subscriptionString = MESSAGE_SUBSCRIPTION.toString()
    expect(subscriptionString).toContain('subscription NewMessage')
    expect(subscriptionString).toContain('messageAdded(channelId: $channelId)')
  })

  it('should handle subscription events', async () => {
    const MESSAGE_SUBSCRIPTION = b.subscription('NewMessage', {
      channelId: 'ID!'
    }, (b, v) => [
      b.messageAdded({ channelId: v.channelId }, b => [
        b.id(),
        b.content(),
      ])
    ])

    const mockSubscription = {
      messageAdded: async function* ({ channelId }) {
        yield { messageAdded: { id: '1', content: 'Hello' } }
        yield { messageAdded: { id: '2', content: 'World' } }
      }
    }

    const subscription = await subscribe({
      schema,
      document: MESSAGE_SUBSCRIPTION.document(),
      variableValues: { channelId: 'channel-1' },
      rootValue: mockSubscription
    })

    const messages = []
    for await (const result of subscription) {
      messages.push(result.data?.messageAdded)
      if (messages.length >= 2) break
    }

    expect(messages).toHaveLength(2)
    expect(messages[0].content).toBe('Hello')
    expect(messages[1].content).toBe('World')
  })
})

Performance Testing

Query Complexity Testing

typescript
describe('Performance Tests', () => {
  it('should not exceed complexity limits', () => {
    const COMPLEX_QUERY = b.query('ComplexQuery', (b) => [
      b.posts(b => [
        b.id(),
        b.comments(b => [
          b.id(),
          b.author(b => [
            b.id(),
            b.posts(b => [  // This creates high complexity
              b.id(),
            ])
          ])
        ])
      ])
    ])

    // Calculate query complexity (simplified)
    const queryString = COMPLEX_QUERY.toString()
    const nestingLevel = (queryString.match(/{/g) || []).length
    
    expect(nestingLevel).toBeLessThan(6) // Complexity limit
  })

  it('should execute within time limits', async () => {
    const TIMED_QUERY = b.query('TimedQuery', (b) => [
      b.posts(b => [
        b.id(),
        b.title(),
        b.author(b => [
          b.name(),
        ])
      ])
    ])

    const startTime = performance.now()
    
    await graphql({
      schema,
      source: TIMED_QUERY.toString(),
      rootValue
    })
    
    const duration = performance.now() - startTime
    expect(duration).toBeLessThan(100) // 100ms limit
  })
})

Load Testing

typescript
describe('Load Tests', () => {
  it('should handle concurrent requests', async () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [
        b.id(),
        b.name(),
      ])
    ])

    const concurrentRequests = Array.from({ length: 10 }, (_, i) =>
      graphql({
        schema,
        source: USER_QUERY.toString(),
        variableValues: { id: String(i % 2 + 1) },
        rootValue
      })
    )

    const results = await Promise.all(concurrentRequests)
    
    results.forEach(result => {
      expect(result.errors).toBeUndefined()
      expect(result.data?.user).toBeDefined()
    })
  })

  it('should handle large datasets', async () => {
    // Create large mock dataset
    const largeMockData = {
      posts: Array.from({ length: 1000 }, (_, i) => ({
        id: String(i),
        title: `Post ${i}`,
        authorId: String(i % 10)
      }))
    }

    const LARGE_QUERY = b.query('GetManyPosts', (b) => [
      b.posts({ first: 100 }, b => [
        b.id(),
        b.title(),
      ])
    ])

    const startTime = performance.now()
    
    const result = await graphql({
      schema,
      source: LARGE_QUERY.toString(),
      rootValue: {
        posts: () => largeMockData.posts.slice(0, 100)
      }
    })
    
    const duration = performance.now() - startTime
    
    expect(result.data?.posts).toHaveLength(100)
    expect(duration).toBeLessThan(500) // Should handle large data efficiently
  })
})

Test Utilities

Custom Test Helpers

typescript
// test-utils.ts
export class QueryTester {
  static validateQuery(query: any) {
    const document = query.document()
    expect(document.kind).toBe('Document')
    expect(document.definitions).toHaveLength(1)
    return document
  }

  static async executeQuery(query: any, variables = {}) {
    return await graphql({
      schema,
      source: query.toString(),
      variableValues: variables,
      rootValue
    })
  }

  static expectValidResult(result: any) {
    expect(result.errors).toBeUndefined()
    expect(result.data).toBeDefined()
    return result.data
  }

  static expectErrorResult(result: any, expectedErrorCount = 1) {
    expect(result.errors).toHaveLength(expectedErrorCount)
    return result.errors
  }
}

// Usage in tests
describe('Query Tests with Utilities', () => {
  it('should validate and execute query', async () => {
    const USER_QUERY = b.query('GetUser', { id: 'ID!' }, (b, v) => [
      b.user({ id: v.id }, b => [b.id(), b.name()])
    ])

    QueryTester.validateQuery(USER_QUERY)
    
    const result = await QueryTester.executeQuery(USER_QUERY, { id: '1' })
    const data = QueryTester.expectValidResult(result)
    
    expect(data.user.name).toBe('John Doe')
  })
})

Mock Data Factories

typescript
// mock-factories.ts
export class MockDataFactory {
  static createUser(overrides = {}) {
    return {
      __typename: 'User',
      id: Math.random().toString(),
      name: 'Test User',
      email: 'test@example.com',
      ...overrides
    }
  }

  static createPost(authorId?: string, overrides = {}) {
    return {
      __typename: 'Post',
      id: Math.random().toString(),
      title: 'Test Post',
      content: 'Test content',
      authorId: authorId || '1',
      ...overrides
    }
  }

  static createQueryResponse<T>(data: T, errors?: any[]) {
    return {
      data,
      errors,
      loading: false,
      networkStatus: 7
    }
  }
}

// Usage
const mockUser = MockDataFactory.createUser({ name: 'John Doe' })
const mockResponse = MockDataFactory.createQueryResponse({ user: mockUser })

Test Configuration

Jest/Vitest Configuration

typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    setupFiles: ['./test-setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'test/',
        '**/*.d.ts',
        '**/generated/**'
      ]
    }
  }
})

// test-setup.ts
import { expect } from 'vitest'

// Custom matchers
expect.extend({
  toBeValidGraphQLQuery(received) {
    const isValid = received && 
                   received.document && 
                   typeof received.toString === 'function'
    
    return {
      pass: isValid,
      message: () => `Expected ${received} to be a valid GraphQL query`
    }
  }
})

Continuous Testing

GitHub Actions Example

yaml
# .github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'pnpm'
      
      - run: pnpm install
      
      - run: pnpm run generate  # Generate GQLB types
      
      - run: pnpm run test      # Run tests
      
      - run: pnpm run test:coverage
      
      - uses: codecov/codecov-action@v3
        with:
          file: ./coverage/coverage-final.json

Next Steps

Released under the MIT License.