Back to Articles
Testing StrategiesIntermediate

GraphQL API Testing

Test GraphQL APIs effectively with queries, mutations, and subscriptions

7 min read
...
graphqlapi-testingtesting
Banner for GraphQL API Testing

Introduction

GraphQL APIs require different testing approaches compared to REST APIs. This guide covers strategies for testing queries, mutations, subscriptions, and GraphQL-specific challenges.

GraphQL Fundamentals for Testing

Key Differences from REST

REST API:

  • Multiple endpoints (one per resource)
  • Fixed response structure
  • Over-fetching or under-fetching common

GraphQL API:

  • Single endpoint (/graphql)
  • Flexible queries (request exactly what you need)
  • Strong typing with schema

GraphQL Operation Types

  1. Query - Read data (GET equivalent)
  2. Mutation - Modify data (POST/PUT/DELETE equivalent)
  3. Subscription - Real-time updates via WebSocket

Testing GraphQL Queries

Basic Query Testing

// Example GraphQL query
const query = `
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        title
        createdAt
      }
    }
  }
`;
 
// Test with fetch
test('should fetch user data', async () => {
  const response = await fetch('https://api.example.com/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query,
      variables: { id: '123' }
    })
  });
  
  const { data, errors } = await response.json();
  
  expect(errors).toBeUndefined();
  expect(data.user).toBeDefined();
  expect(data.user.id).toBe('123');
  expect(data.user.posts).toBeInstanceOf(Array);
});

Using GraphQL Client Libraries

// Using Apollo Client in tests
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
 
const client = new ApolloClient({
  uri: 'https://api.example.com/graphql',
  cache: new InMemoryCache()
});
 
test('query with Apollo Client', async () => {
  const GET_USER = gql`
    query GetUser($id: ID!) {
      user(id: $id) {
        id
        name
        email
      }
    }
  `;
  
  const { data } = await client.query({
    query: GET_USER,
    variables: { id: '123' }
  });
  
  expect(data.user.name).toBe('John Doe');
});

Testing Nested Queries

test('should fetch nested data correctly', async () => {
  const query = `
    query {
      organization(id: "org1") {
        name
        teams {
          name
          members {
            name
            role
          }
        }
      }
    }
  `;
  
  const { data } = await executeQuery(query);
  
  expect(data.organization.teams).toHaveLength(3);
  expect(data.organization.teams[0].members).toBeDefined();
});

Testing GraphQL Mutations

Basic Mutation Testing

test('should create a new post', async () => {
  const mutation = `
    mutation CreatePost($input: CreatePostInput!) {
      createPost(input: $input) {
        id
        title
        content
        author {
          name
        }
      }
    }
  `;
  
  const variables = {
    input: {
      title: 'Test Post',
      content: 'This is a test',
      authorId: '123'
    }
  };
  
  const { data, errors } = await executeMutation(mutation, variables);
  
  expect(errors).toBeUndefined();
  expect(data.createPost.id).toBeDefined();
  expect(data.createPost.title).toBe('Test Post');
});

Testing Mutation Side Effects

test('mutation should update database', async () => {
  // Create a post
  const createMutation = `
    mutation {
      createPost(input: { title: "New Post", content: "Content" }) {
        id
      }
    }
  `;
  
  const { data: createData } = await executeMutation(createMutation);
  const postId = createData.createPost.id;
  
  // Verify it was created by querying
  const query = `
    query {
      post(id: "${postId}") {
        title
        content
      }
    }
  `;
  
  const { data: queryData } = await executeQuery(query);
  expect(queryData.post.title).toBe('New Post');
});

Testing GraphQL Subscriptions

WebSocket Subscription Testing

import { createClient } from 'graphql-ws';
 
test('should receive real-time updates', async () => {
  const client = createClient({
    url: 'ws://api.example.com/graphql'
  });
  
  const subscription = `
    subscription OnPostCreated {
      postCreated {
        id
        title
        author {
          name
        }
      }
    }
  `;
  
  const messages = [];
  
  await new Promise((resolve) => {
    client.subscribe(
      { query: subscription },
      {
        next: (data) => {
          messages.push(data);
          if (messages.length === 2) resolve();
        },
        error: (err) => { throw err; }
      }
    );
    
    // Trigger events that should cause subscriptions
    setTimeout(() => createPost({ title: 'Post 1' }), 100);
    setTimeout(() => createPost({ title: 'Post 2' }), 200);
  });
  
  expect(messages).toHaveLength(2);
  expect(messages[0].data.postCreated.title).toBe('Post 1');
});

Schema Validation Testing

Introspection Query Testing

test('should validate schema types', async () => {
  const introspectionQuery = `
    query {
      __schema {
        types {
          name
          kind
          fields {
            name
            type {
              name
            }
          }
        }
      }
    }
  `;
  
  const { data } = await executeQuery(introspectionQuery);
  
  const userType = data.__schema.types.find(t => t.name === 'User');
  expect(userType).toBeDefined();
  expect(userType.fields.find(f => f.name === 'email')).toBeDefined();
});

Schema Compatibility Testing

// Test backward compatibility when schema changes
test('deprecated fields still work', async () => {
  const query = `
    query {
      user(id: "123") {
        oldFieldName @deprecated(reason: "Use newFieldName instead")
        newFieldName
      }
    }
  `;
  
  const { data, errors } = await executeQuery(query);
  
  // Both should work during deprecation period
  expect(data.user.oldFieldName).toBe(data.user.newFieldName);
});

Error Handling Testing

Testing GraphQL Errors

test('should handle validation errors', async () => {
  const query = `
    query {
      user(id: "invalid-id-format") {
        name
      }
    }
  `;
  
  const { data, errors } = await executeQuery(query);
  
  expect(errors).toBeDefined();
  expect(errors[0].message).toContain('Invalid ID format');
  expect(errors[0].extensions.code).toBe('VALIDATION_ERROR');
});
 
test('should handle not found errors', async () => {
  const query = `
    query {
      user(id: "999999") {
        name
      }
    }
  `;
  
  const { data, errors } = await executeQuery(query);
  
  expect(data.user).toBeNull();
  expect(errors[0].extensions.code).toBe('NOT_FOUND');
});

Testing Partial Errors

test('should handle partial success', async () => {
  const query = `
    query {
      validUser: user(id: "123") { name }
      invalidUser: user(id: "999") { name }
    }
  `;
  
  const { data, errors } = await executeQuery(query);
  
  // One field succeeds, one fails
  expect(data.validUser).toBeDefined();
  expect(data.invalidUser).toBeNull();
  expect(errors).toHaveLength(1);
});

Performance Testing

Query Complexity Testing

test('should reject overly complex queries', async () => {
  const complexQuery = `
    query {
      users {
        posts {
          comments {
            author {
              posts {
                comments {
                  # Too many nested levels
                }
              }
            }
          }
        }
      }
    }
  `;
  
  const { errors } = await executeQuery(complexQuery);
  
  expect(errors[0].extensions.code).toBe('QUERY_TOO_COMPLEX');
});

N+1 Query Problem Testing

test('should efficiently fetch nested data', async () => {
  const startTime = Date.now();
  
  const query = `
    query {
      posts(limit: 10) {
        title
        author {
          name
        }
      }
    }
  `;
  
  await executeQuery(query);
  const duration = Date.now() - startTime;
  
  // Should use DataLoader or similar batching
  expect(duration).toBeLessThan(1000);
});

Authorization Testing

Testing Field-Level Permissions

test('unauthorized user cannot access private data', async () => {
  const query = `
    query {
      user(id: "123") {
        name
        email  # Only owner can see
      }
    }
  `;
  
  const { data, errors } = await executeQuery(query, {
    headers: { Authorization: 'Bearer unauthorized-token' }
  });
  
  expect(data.user.name).toBeDefined();
  expect(data.user.email).toBeNull();
  expect(errors[0].message).toContain('Not authorized');
});

Best Practices

1. Test Data Management

  • Use GraphQL operations to set up test data
  • Clean up after tests with delete mutations
  • Use GraphQL transactions if available

2. Organize Tests by Features

describe('User Operations', () => {
  describe('Queries', () => {
    test('getUser', () => {});
    test('listUsers', () => {});
  });
  
  describe('Mutations', () => {
    test('createUser', () => {});
    test('updateUser', () => {});
  });
});

3. Use Fragments for Reusability

const USER_FRAGMENT = `
  fragment UserFields on User {
    id
    name
    email
  }
`;
 
const query = `
  ${USER_FRAGMENT}
  query {
    user(id: "123") {
      ...UserFields
    }
  }
`;

4. Mock GraphQL Responses

import { MockedProvider } from '@apollo/client/testing';
 
const mocks = [{
  request: {
    query: GET_USER,
    variables: { id: '123' }
  },
  result: {
    data: { user: { id: '123', name: 'John' } }
  }
}];
 
// Use in tests
<MockedProvider mocks={mocks}>
  <UserComponent />
</MockedProvider>

Tools and Libraries

  • Apollo Client - Full-featured GraphQL client
  • graphql-request - Minimal GraphQL client
  • EasyGraphQL Tester - Schema-based testing
  • GraphQL Playground - Interactive testing
  • Insomnia/Postman - Manual API testing

Conclusion

GraphQL testing requires understanding of queries, mutations, subscriptions, and schema validation. Use proper error handling, test authorization, and monitor performance to ensure API quality.

Next Steps

  1. Explore your GraphQL schema with introspection
  2. Write tests for critical queries and mutations
  3. Set up subscription testing for real-time features
  4. Implement schema validation in CI/CD
  5. Monitor query performance and complexity

Comments (0)

Loading comments...