GraphQL (как обсуждается в API Testing Architecture: From Monoliths to Microservices) стал популярной альтернативой REST API, предлагая гибкую выборку данных и строгую типизацию. Однако тестирование GraphQL API требует других подходов, чем традиционное тестирование REST (как обсуждается в gRPC Testing: Comprehensive Guide for RPC API Testing). Это подробное руководство охватывает все, что нужно знать об эффективном тестировании GraphQL API.
Основы тестирования GraphQL
Тестирование GraphQL отличается от тестирования REST API в нескольких ключевых аспектах. В отличие от REST, где каждая конечная точка возвращает фиксированную структуру данных, GraphQL позволяет клиентам запрашивать именно те данные, которые им нужны, через единственную конечную точку. Эта гибкость создает уникальные задачи тестирования.
Ключевые отличия от тестирования REST
Аспект | Тестирование REST | Тестирование GraphQL |
---|---|---|
Конечные точки | Множество конечных точек | Единственная конечная точка |
Структура данных | Фиксированная на конечную точку | Динамическая на запрос |
Over-fetching | Частая проблема | Устранена дизайном |
Under-fetching | Требует множественных запросов | Решается одним запросом |
Версионирование | Версии на основе URL | Эволюция схемы |
Обработка ошибок | HTTP-коды статуса | Возможен частичный успех |
Природа единственной конечной точки GraphQL означает, что традиционное тестирование HTTP-кодов статуса менее релевантно. Запрос GraphQL может вернуть HTTP 200 даже с ошибками в данных ответа.
Стратегии тестирования запросов
Тестирование запросов формирует основу тестирования GraphQL. Запросы позволяют клиентам получать данные, и их тестирование обеспечивает корректную выборку данных и правильное разрешение полей.
Базовое тестирование запросов
Начните с простых тестов запросов, проверяющих базовую функциональность:
// Пример использования Jest и Apollo Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
describe('Тесты запросов пользователей', () => {
let client;
beforeAll(() => {
client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache()
});
});
test('должен получить пользователя по 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}$/);
});
});
Тестирование вложенных запросов
Сила GraphQL заключается во вложенных запросах. Тестируйте глубокую вложенность запросов, чтобы убедиться, что резолверы работают корректно:
test('должен получить пользователя с вложенными постами и комментариями', 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();
});
Тестирование производительности запросов
Тестируйте производительность запросов, особенно для сложных вложенных запросов, которые могут вызвать проблему N+1:
test('должен эффективно разрешать вложенные данные без 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;
// Должно завершиться менее чем за 500мс даже с вложенными данными
expect(duration).toBeLessThan(500);
expect(data.user.posts.length).toBeGreaterThan(0);
});
Тестирование мутаций
Мутации изменяют данные на стороне сервера. Тестирование мутаций требует проверки как немедленного ответа, так и побочных эффектов.
Тестирование мутаций создания
test('должен создать нового пользователя', 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: 'Иван Иванов',
email: 'ivan@example.com',
password: 'БезопасныйПароль123!'
}
}
});
expect(data.createUser.id).toBeDefined();
expect(data.createUser.name).toBe('Иван Иванов');
expect(data.createUser.email).toBe('ivan@example.com');
expect(new Date(data.createUser.createdAt)).toBeInstanceOf(Date);
});
Тестирование мутаций обновления
Проверьте, что обновления работают корректно и возвращают обновленные данные:
test('должен обновить профиль пользователя', 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: 'Мария Иванова',
bio: 'Обновленный текст биографии'
}
}
});
expect(data.updateUser.name).toBe('Мария Иванова');
expect(data.updateUser.bio).toBe('Обновленный текст биографии');
});
Тестирование мутаций удаления
Тестируйте мутации удаления и проверяйте правильную очистку:
test('должен удалить пользователя и каскадно связанные данные', 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);
// Проверить, что пользователь больше не существует
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
}
}
`;
await expect(
client.query({ query: GET_USER, variables: { id: '123' } })
).rejects.toThrow();
});
Тестирование подписок
Подписки GraphQL обеспечивают обновления данных в реальном времени через WebSockets. Тестирование подписок требует других техник.
Тестирование WebSocket подписок
import { WebSocketLink } from '@apollo/client/link/ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import ws from 'ws';
test('должен получать обновления в реальном времени через подписку', (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);
}
});
// Вызвать мутацию для генерации события подписки
setTimeout(() => {
client.mutate({
mutation: gql`
mutation {
sendMessage(channelId: "channel-1", content: "Тестовое сообщение") {
id
}
}
`
});
}, 100);
}, 10000);
Тестирование валидации схемы
Валидация схемы обеспечивает, что ваш GraphQL API поддерживает консистентные определения типов и следует лучшим практикам.
Линтинг схемы
Используйте GraphQL Inspector или аналогичные инструменты для валидации изменений схемы:
import { buildSchema } from 'graphql';
import { findBreakingChanges, findDangerousChanges } from 'graphql';
test('не должен вводить ломающие изменения схемы', () => {
const oldSchema = buildSchema(oldSchemaSDL);
const newSchema = buildSchema(newSchemaSDL);
const breakingChanges = findBreakingChanges(oldSchema, newSchema);
const dangerousChanges = findDangerousChanges(oldSchema, newSchema);
expect(breakingChanges).toHaveLength(0);
// Логировать опасные изменения для ревью
if (dangerousChanges.length > 0) {
console.warn('Обнаружены опасные изменения схемы:', dangerousChanges);
}
});
Тестирование покрытия типов
Убедитесь, что все типы имеют правильные описания и обязательные поля:
test('должен иметь описания для всех типов и полей', () => {
const schema = buildSchema(schemaSDL);
const typeMap = schema.getTypeMap();
Object.keys(typeMap).forEach(typeName => {
// Пропустить встроенные типы
if (typeName.startsWith('__')) return;
const type = typeMap[typeName];
if (type.description === undefined) {
throw new Error(`Тип ${typeName} без описания`);
}
// Проверить описания полей для объектных типов
if ('getFields' in type) {
const fields = type.getFields();
Object.keys(fields).forEach(fieldName => {
if (!fields[fieldName].description) {
throw new Error(`Поле ${typeName}.${fieldName} без описания`);
}
});
}
});
});
Мокирование ответов GraphQL
Мокирование необходимо для тестирования клиентских приложений без зависимости от работающего сервера.
Мокирование с Apollo Client
import { MockedProvider } from '@apollo/client/testing';
const mocks = [
{
request: {
query: GET_USER,
variables: { id: '123' }
},
result: {
data: {
user: {
id: '123',
name: 'Иван Иванов',
email: 'ivan@example.com'
}
}
}
}
];
test('должен отрендерить данные пользователя из замоканного запроса', async () => {
const { getByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<UserProfile userId="123" />
</MockedProvider>
);
// Дождаться загрузки данных
await waitFor(() => {
expect(getByText('Иван Иванов')).toBeInTheDocument();
});
});
Mock Service Worker (MSW) для 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: 'Замоканный Пользователь',
email: 'mock@example.com'
}
})
);
}),
graphql.mutation('CreateUser', (req, res, ctx) => {
const { input } = req.variables;
return res(
ctx.data({
createUser: {
id: 'новый-id',
...input,
createdAt: new Date().toISOString()
}
})
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Тестирование обработки ошибок
GraphQL имеет уникальные характеристики обработки ошибок. Ошибки могут возникать на множестве уровней.
Тестирование ошибок на уровне полей
test('должен корректно обрабатывать ошибки на уровне полей', 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();
});
Тестирование сетевых ошибок
test('должен обрабатывать сетевые ошибки', async () => {
const errorLink = onError(({ networkError, graphQLErrors }) => {
if (networkError) {
console.log(`[Сетевая ошибка]: ${networkError}`);
}
});
const clientWithErrorHandling = new ApolloClient({
link: from([errorLink, httpLink]),
cache: new InMemoryCache()
});
await expect(
clientWithErrorHandling.query({
query: GET_USER,
variables: { id: 'invalid' }
})
).rejects.toThrow();
});
Интеграционное тестирование с базами данных
Тестируйте резолверы GraphQL с реальными операциями базы данных:
import { MongoMemoryServer } from 'mongodb-memory-server';
describe('Интеграция GraphQL с базой данных', () => {
let mongoServer;
let db;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const uri = mongoServer.getUri();
// Подключиться к MongoDB в памяти
});
afterAll(async () => {
await mongoServer.stop();
});
test('должен сохранить данные пользователя', async () => {
const { data } = await client.mutate({
mutation: CREATE_USER,
variables: { input: { name: 'Тестовый Пользователь', email: 'test@example.com' } }
});
// Проверить в базе данных
const dbUser = await db.collection('users').findOne({ _id: data.createUser.id });
expect(dbUser.name).toBe('Тестовый Пользователь');
});
});
Лучшие практики и рекомендации
Чеклист тестирования
- ✅ Тестировать все вариации запросов с разными комбинациями аргументов
- ✅ Проверять побочные эффекты мутаций и сохранение данных
- ✅ Тестировать поведение подписок в реальном времени и очистку
- ✅ Валидировать изменения схемы на ломающие модификации
- ✅ Консистентно мокировать внешние зависимости
- ✅ Тестировать сценарии ошибок на уровне полей и запросов
- ✅ Проверять правила авторизации и аутентификации
- ✅ Тестировать пагинацию и паттерны соединений
- ✅ Валидировать санитизацию и валидацию входных данных
- ✅ Мониторить производительность и сложность запросов
Советы по тестированию производительности
Используйте анализ сложности запросов для предотвращения дорогих запросов:
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const schema = makeExecutableSchema({
typeDefs,
resolvers
});
const complexityLimit = createComplexityLimitRule(1000);
test('должен отклонять чрезмерно сложные запросы', () => {
const complexQuery = gql`...`; // Очень глубоко вложенный запрос
const errors = validate(schema, parse(complexQuery), [complexityLimit]);
expect(errors.length).toBeGreaterThan(0);
expect(errors[0].message).toContain('превышает максимальную сложность');
});
Заключение
Тестирование GraphQL требует понимания уникальных характеристик протокола — единственные конечные точки, гибкие запросы, подписки в реальном времени и частичные ошибки. Реализуя комплексные стратегии тестирования для запросов, мутаций, подписок, валидации схемы и обработки ошибок, вы можете обеспечить надежность и производительность вашего GraphQL API.
Сосредоточьтесь на тестировании бизнес-логики в резолверах, валидации эволюции схемы и обеспечении правильной обработки ошибок на всех уровнях. Широко используйте мокирование для клиентских тестов и интеграционное тестирование для критических потоков данных. С этими практиками ваш GraphQL API будет надежным и поддерживаемым.