GraphQL (como se discute en API Testing Architecture: From Monoliths to Microservices) se ha convertido en una alternativa popular a las APIs REST, ofreciendo obtención flexible de datos y tipado fuerte. Sin embargo, probar APIs GraphQL requiere enfoques diferentes a las pruebas REST (como se discute en gRPC Testing: Comprehensive Guide for RPC API Testing) tradicionales. Esta guía completa cubre todo lo que necesitas saber sobre probar APIs GraphQL de manera efectiva.

Fundamentos del Testing de GraphQL

El testing de GraphQL difiere del testing de APIs REST en varios aspectos clave. A diferencia de REST, donde cada endpoint devuelve una estructura de datos fija, GraphQL permite a los clientes solicitar exactamente los datos que necesitan a través de un único endpoint. Esta flexibilidad crea desafíos únicos de testing.

Diferencias clave con el Testing REST

AspectoTesting RESTTesting GraphQL
EndpointsMúltiples endpointsEndpoint único
Estructura de DatosFija por endpointDinámica por query
Over-fetchingProblema comúnEliminado por diseño
Under-fetchingRequiere múltiples peticionesResuelto en una sola query
VersionadoVersiones basadas en URLEvolución del schema
Manejo de ErroresCódigos de estado HTTPÉxito parcial posible

La naturaleza de endpoint único de GraphQL significa que las pruebas tradicionales de códigos de estado HTTP son menos relevantes. Una petición GraphQL puede devolver HTTP 200 incluso con errores en los datos de respuesta.

Estrategias de Testing de Queries

El testing de queries forma la base del testing de GraphQL. Las queries permiten a los clientes obtener datos, y probarlas asegura la recuperación correcta de datos y la resolución adecuada de campos.

Testing Básico de Queries

Comienza con pruebas simples de queries que verifican funcionalidad básica:

// Ejemplo usando Jest y Apollo Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

describe('Pruebas de Query de Usuario', () => {
  let client;

  beforeAll(() => {
    client = new ApolloClient({
      uri: 'http://localhost:4000/graphql',
      cache: new InMemoryCache()
    });
  });

  test('debe obtener usuario por ID', 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).toBeDefined();
    expect(data.user.id).toBe('123');
    expect(data.user.name).toBeTruthy();
    expect(data.user.email).toMatch(/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/);
  });
});

Testing de Queries Anidadas

El poder de GraphQL radica en las queries anidadas. Prueba el anidamiento profundo de queries para asegurar que los resolvers funcionen correctamente:

test('debe obtener usuario con posts y comentarios anidados', async () => {
  const GET_USER_WITH_POSTS = gql`
    query GetUserWithPosts($id: ID!) {
      user(id: $id) {
        id
        name
        posts {
          id
          title
          comments {
            id
            content
            author {
              name
            }
          }
        }
      }
    }
  `;

  const { data } = await client.query({
    query: GET_USER_WITH_POSTS,
    variables: { id: '123' }
  });

  expect(data.user.posts).toBeInstanceOf(Array);
  expect(data.user.posts[0].comments).toBeInstanceOf(Array);
  expect(data.user.posts[0].comments[0].author.name).toBeDefined();
});

Testing de Rendimiento de Queries

Prueba el rendimiento de queries, especialmente para queries anidadas complejas que podrían desencadenar el problema N+1:

test('debe resolver datos anidados eficientemente sin queries N+1', async () => {
  const startTime = Date.now();

  const { data } = await client.query({
    query: GET_USER_WITH_POSTS,
    variables: { id: '123' }
  });

  const duration = Date.now() - startTime;

  // Debe completarse en menos de 500ms incluso con datos anidados
  expect(duration).toBeLessThan(500);
  expect(data.user.posts.length).toBeGreaterThan(0);
});

Testing de Mutaciones

Las mutaciones modifican datos del lado del servidor. Probar mutaciones requiere verificar tanto la respuesta inmediata como los efectos secundarios.

Testing de Mutaciones de Creación

test('debe crear nuevo usuario', async () => {
  const CREATE_USER = gql`
    mutation CreateUser($input: UserInput!) {
      createUser(input: $input) {
        id
        name
        email
        createdAt
      }
    }
  `;

  const { data } = await client.mutate({
    mutation: CREATE_USER,
    variables: {
      input: {
        name: 'Juan Pérez',
        email: 'juan@ejemplo.com',
        password: 'ClaveSegura123!'
      }
    }
  });

  expect(data.createUser.id).toBeDefined();
  expect(data.createUser.name).toBe('Juan Pérez');
  expect(data.createUser.email).toBe('juan@ejemplo.com');
  expect(new Date(data.createUser.createdAt)).toBeInstanceOf(Date);
});

Testing de Mutaciones de Actualización

Verifica que las actualizaciones funcionen correctamente y devuelvan datos actualizados:

test('debe actualizar perfil de usuario', async () => {
  const UPDATE_USER = gql`
    mutation UpdateUser($id: ID!, $input: UserUpdateInput!) {
      updateUser(id: $id, input: $input) {
        id
        name
        bio
        updatedAt
      }
    }
  `;

  const { data } = await client.mutate({
    mutation: UPDATE_USER,
    variables: {
      id: '123',
      input: {
        name: 'Juana Pérez',
        bio: 'Texto de biografía actualizado'
      }
    }
  });

  expect(data.updateUser.name).toBe('Juana Pérez');
  expect(data.updateUser.bio).toBe('Texto de biografía actualizado');
});

Testing de Mutaciones de Eliminación

Prueba mutaciones de eliminación y verifica la limpieza adecuada:

test('debe eliminar usuario y datos relacionados en cascada', async () => {
  const DELETE_USER = gql`
    mutation DeleteUser($id: ID!) {
      deleteUser(id: $id) {
        success
        message
      }
    }
  `;

  const { data } = await client.mutate({
    mutation: DELETE_USER,
    variables: { id: '123' }
  });

  expect(data.deleteUser.success).toBe(true);

  // Verificar que el usuario ya no existe
  const GET_USER = gql`
    query GetUser($id: ID!) {
      user(id: $id) {
        id
      }
    }
  `;

  await expect(
    client.query({ query: GET_USER, variables: { id: '123' } })
  ).rejects.toThrow();
});

Testing de Subscripciones

Las subscripciones de GraphQL permiten actualizaciones de datos en tiempo real vía WebSockets. Probar subscripciones requiere técnicas diferentes.

Testing de Subscripciones WebSocket

import { WebSocketLink } from '@apollo/client/link/ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import ws from 'ws';

test('debe recibir actualizaciones en tiempo real vía subscripción', (done) => {
  const wsClient = new SubscriptionClient(
    'ws://localhost:4000/graphql',
    { reconnect: true },
    ws
  );

  const NEW_MESSAGE_SUBSCRIPTION = gql`
    subscription OnNewMessage($channelId: ID!) {
      messageAdded(channelId: $channelId) {
        id
        content
        author {
          name
        }
        createdAt
      }
    }
  `;

  const observable = wsClient.request({
    query: NEW_MESSAGE_SUBSCRIPTION,
    variables: { channelId: 'channel-1' }
  });

  const subscription = observable.subscribe({
    next: (result) => {
      expect(result.data.messageAdded).toBeDefined();
      expect(result.data.messageAdded.content).toBeTruthy();
      subscription.unsubscribe();
      wsClient.close();
      done();
    },
    error: (error) => {
      done(error);
    }
  });

  // Disparar mutación para generar evento de subscripción
  setTimeout(() => {
    client.mutate({
      mutation: gql`
        mutation {
          sendMessage(channelId: "channel-1", content: "Mensaje de prueba") {
            id
          }
        }
      `
    });
  }, 100);
}, 10000);

Testing de Validación de Schema

La validación de schema asegura que tu API GraphQL mantenga definiciones de tipo consistentes y siga mejores prácticas.

Linting de Schema

Usa GraphQL Inspector o herramientas similares para validar cambios de schema:

import { buildSchema } from 'graphql';
import { findBreakingChanges, findDangerousChanges } from 'graphql';

test('no debe introducir cambios de schema que rompan compatibilidad', () => {
  const oldSchema = buildSchema(oldSchemaSDL);
  const newSchema = buildSchema(newSchemaSDL);

  const breakingChanges = findBreakingChanges(oldSchema, newSchema);
  const dangerousChanges = findDangerousChanges(oldSchema, newSchema);

  expect(breakingChanges).toHaveLength(0);

  // Registrar cambios peligrosos para revisión
  if (dangerousChanges.length > 0) {
    console.warn('Cambios peligrosos de schema detectados:', dangerousChanges);
  }
});

Testing de Cobertura de Tipos

Asegura que todos los tipos tengan descripciones adecuadas y campos requeridos:

test('debe tener descripciones para todos los tipos y campos', () => {
  const schema = buildSchema(schemaSDL);
  const typeMap = schema.getTypeMap();

  Object.keys(typeMap).forEach(typeName => {
    // Saltar tipos integrados
    if (typeName.startsWith('__')) return;

    const type = typeMap[typeName];

    if (type.description === undefined) {
      throw new Error(`Tipo ${typeName} sin descripción`);
    }

    // Verificar descripciones de campos para tipos de objeto
    if ('getFields' in type) {
      const fields = type.getFields();
      Object.keys(fields).forEach(fieldName => {
        if (!fields[fieldName].description) {
          throw new Error(`Campo ${typeName}.${fieldName} sin descripción`);
        }
      });
    }
  });
});

Mocking de Respuestas GraphQL

El mocking es esencial para probar aplicaciones cliente sin depender de un servidor en vivo.

Mocking con Apollo Client

import { MockedProvider } from '@apollo/client/testing';

const mocks = [
  {
    request: {
      query: GET_USER,
      variables: { id: '123' }
    },
    result: {
      data: {
        user: {
          id: '123',
          name: 'Juan Pérez',
          email: 'juan@ejemplo.com'
        }
      }
    }
  }
];

test('debe renderizar datos de usuario desde query mockeada', async () => {
  const { getByText } = render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <UserProfile userId="123" />
    </MockedProvider>
  );

  // Esperar a que los datos se carguen
  await waitFor(() => {
    expect(getByText('Juan Pérez')).toBeInTheDocument();
  });
});

Mock Service Worker (MSW) para GraphQL

import { graphql } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  graphql.query('GetUser', (req, res, ctx) => {
    const { id } = req.variables;

    return res(
      ctx.data({
        user: {
          id,
          name: 'Usuario Simulado',
          email: 'simulado@ejemplo.com'
        }
      })
    );
  }),

  graphql.mutation('CreateUser', (req, res, ctx) => {
    const { input } = req.variables;

    return res(
      ctx.data({
        createUser: {
          id: 'nuevo-id',
          ...input,
          createdAt: new Date().toISOString()
        }
      })
    );
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Testing de Manejo de Errores

GraphQL tiene características únicas de manejo de errores. Los errores pueden ocurrir en múltiples niveles.

Testing de Errores a Nivel de Campo

test('debe manejar errores a nivel de campo graciosamente', async () => {
  const { data, errors } = await client.query({
    query: gql`
      query {
        user(id: "123") {
          id
          name
          restrictedField
        }
      }
    `
  });

  expect(errors).toBeDefined();
  expect(errors[0].path).toContain('restrictedField');
  expect(data.user.id).toBe('123');
  expect(data.user.name).toBeDefined();
  expect(data.user.restrictedField).toBeNull();
});

Testing de Errores de Red

test('debe manejar errores de red', async () => {
  const errorLink = onError(({ networkError, graphQLErrors }) => {
    if (networkError) {
      console.log(`[Error de red]: ${networkError}`);
    }
  });

  const clientWithErrorHandling = new ApolloClient({
    link: from([errorLink, httpLink]),
    cache: new InMemoryCache()
  });

  await expect(
    clientWithErrorHandling.query({
      query: GET_USER,
      variables: { id: 'invalid' }
    })
  ).rejects.toThrow();
});

Testing de Integración con Bases de Datos

Prueba resolvers de GraphQL con operaciones reales de base de datos:

import { MongoMemoryServer } from 'mongodb-memory-server';

describe('Integración GraphQL con Base de Datos', () => {
  let mongoServer;
  let db;

  beforeAll(async () => {
    mongoServer = await MongoMemoryServer.create();
    const uri = mongoServer.getUri();
    // Conectar a MongoDB en memoria
  });

  afterAll(async () => {
    await mongoServer.stop();
  });

  test('debe persistir datos de usuario', async () => {
    const { data } = await client.mutate({
      mutation: CREATE_USER,
      variables: { input: { name: 'Usuario Prueba', email: 'prueba@ejemplo.com' } }
    });

    // Verificar en base de datos
    const dbUser = await db.collection('users').findOne({ _id: data.createUser.id });
    expect(dbUser.name).toBe('Usuario Prueba');
  });
});

Mejores Prácticas y Recomendaciones

Checklist de Testing

  • ✅ Probar todas las variaciones de query con diferentes combinaciones de argumentos
  • ✅ Verificar efectos secundarios de mutaciones y persistencia de datos
  • ✅ Probar comportamiento en tiempo real de subscripciones y limpieza
  • ✅ Validar cambios de schema para modificaciones que rompan compatibilidad
  • ✅ Mockear dependencias externas de manera consistente
  • ✅ Probar escenarios de error a nivel de campo y petición
  • ✅ Verificar reglas de autorización y autenticación
  • ✅ Probar paginación y patrones de conexión
  • ✅ Validar sanitización y validación de entrada
  • ✅ Monitorear rendimiento y complejidad de queries

Tips de Testing de Rendimiento

Usa análisis de complejidad de queries para prevenir queries costosas:

import { createComplexityLimitRule } from 'graphql-validation-complexity';

const schema = makeExecutableSchema({
  typeDefs,
  resolvers
});

const complexityLimit = createComplexityLimitRule(1000);

test('debe rechazar queries excesivamente complejas', () => {
  const complexQuery = gql`...`; // Query muy anidada

  const errors = validate(schema, parse(complexQuery), [complexityLimit]);

  expect(errors.length).toBeGreaterThan(0);
  expect(errors[0].message).toContain('excede la complejidad máxima');
});

Conclusión

El testing de GraphQL requiere entender las características únicas del protocolo: endpoints únicos, queries flexibles, subscripciones en tiempo real y errores parciales. Al implementar estrategias comprensivas de testing para queries, mutaciones, subscripciones, validación de schema y manejo de errores, puedes asegurar que tu API GraphQL sea confiable y eficiente.

Enfócate en probar la lógica de negocio dentro de los resolvers, validar la evolución del schema y asegurar el manejo apropiado de errores en todos los niveles. Usa mocking extensivamente para pruebas del lado del cliente, y testing de integración para flujos de datos críticos. Con estas prácticas implementadas, tu API GraphQL será robusta y mantenible.