واجهات GraphQL
اختبار واجهات برمجة تطبيقات GraphQL
لماذا نختبر واجهات برمجة تطبيقات GraphQL؟
يضمن اختبار واجهات برمجة تطبيقات GraphQL أن المحللات الخاصة بك ترجع البيانات الصحيحة، وتتعامل مع الأخطاء بشكل أنيق، وتحافظ على الأداء تحت الحمل. على عكس واجهات برمجة تطبيقات REST ذات نقاط النهاية الثابتة، يتطلب هيكل استعلام GraphQL المرن استراتيجيات اختبار شاملة تغطي التحقق من صحة المخطط، ومنطق المحلل، وسيناريوهات التكامل.
إعداد الاختبار
قم بتثبيت مكتبات الاختبار الضرورية:
npm install --save-dev jest @types/jest
npm install --save-dev @apollo/server
npm install --save-dev graphql-tag
npm install --save-dev supertest
// package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"]
}
}
اختبار وحدة المحللات
اختبر المحللات الفردية بشكل منعزل:
// resolvers/userResolvers.js
const resolvers = {
Query: {
user: async (parent, { id }, { dataSources }) => {
return dataSources.userAPI.getUser(id);
},
users: async (parent, args, { dataSources }) => {
return dataSources.userAPI.getAllUsers();
}
},
User: {
posts: async (user, args, { dataSources }) => {
return dataSources.postAPI.getPostsByUserId(user.id);
},
fullName: (user) => {
return `${user.firstName} ${user.lastName}`;
}
},
Mutation: {
createUser: async (parent, { input }, { dataSources }) => {
const newUser = await dataSources.userAPI.createUser(input);
return {
success: true,
message: 'تم إنشاء المستخدم بنجاح',
user: newUser
};
}
}
};
module.exports = resolvers;
// __tests__/userResolvers.test.js
const resolvers = require('../resolvers/userResolvers');
describe('محللات المستخدم', () => {
describe('Query.user', () => {
it('يجب أن يعيد مستخدمًا بواسطة ID', async () => {
// مصدر بيانات وهمي
const mockUserAPI = {
getUser: jest.fn().mockResolvedValue({
id: '1',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
})
};
const context = {
dataSources: { userAPI: mockUserAPI }
};
const result = await resolvers.Query.user(
null,
{ id: '1' },
context
);
expect(mockUserAPI.getUser).toHaveBeenCalledWith('1');
expect(result).toEqual({
id: '1',
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
});
});
it('يجب أن يتعامل مع الأخطاء عندما لا يتم العثور على المستخدم', async () => {
const mockUserAPI = {
getUser: jest.fn().mockRejectedValue(new Error('المستخدم غير موجود'))
};
const context = {
dataSources: { userAPI: mockUserAPI }
};
await expect(
resolvers.Query.user(null, { id: '999' }, context)
).rejects.toThrow('المستخدم غير موجود');
});
});
describe('User.fullName', () => {
it('يجب أن يدمج firstName و lastName', () => {
const user = {
firstName: 'Jane',
lastName: 'Smith'
};
const result = resolvers.User.fullName(user);
expect(result).toBe('Jane Smith');
});
});
describe('Mutation.createUser', () => {
it('يجب أن ينشئ مستخدمًا جديدًا', async () => {
const mockUserAPI = {
createUser: jest.fn().mockResolvedValue({
id: '2',
firstName: 'Alice',
lastName: 'Johnson',
email: 'alice@example.com'
})
};
const context = {
dataSources: { userAPI: mockUserAPI }
};
const input = {
firstName: 'Alice',
lastName: 'Johnson',
email: 'alice@example.com'
};
const result = await resolvers.Mutation.createUser(
null,
{ input },
context
);
expect(mockUserAPI.createUser).toHaveBeenCalledWith(input);
expect(result.success).toBe(true);
expect(result.user.id).toBe('2');
});
});
});
ملاحظة: يجب أن تركز اختبارات الوحدة على منطق المحلل فقط. قم بمحاكاة جميع التبعيات الخارجية مثل قواعد البيانات وواجهات برمجة التطبيقات ومصادر البيانات.
اختبار التكامل مع Apollo Server
اختبر الاستعلامات والطفرات الكاملة مقابل مثيل خادم حقيقي:
// __tests__/integration.test.js
const { ApolloServer } = require('@apollo/server');
const { startStandaloneServer } = require('@apollo/server/standalone');
const gql = require('graphql-tag');
const typeDefs = require('../schema');
const resolvers = require('../resolvers');
describe('اختبارات تكامل GraphQL', () => {
let server;
let url;
beforeAll(async () => {
// إنشاء خادم اختبار
server = new ApolloServer({
typeDefs,
resolvers,
context: async () => ({
dataSources: {
userAPI: new MockUserAPI(),
postAPI: new MockPostAPI()
}
})
});
// بدء الخادم
const { url: serverUrl } = await startStandaloneServer(server, {
listen: { port: 0 } // منفذ متاح عشوائي
});
url = serverUrl;
});
afterAll(async () => {
await server?.stop();
});
it('يجب أن يجلب مستخدمًا مع منشورات متداخلة', async () => {
const query = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
firstName
lastName
fullName
posts {
id
title
}
}
}
`;
const response = await server.executeOperation({
query,
variables: { id: '1' }
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data.user).toMatchObject({
id: '1',
firstName: 'John',
fullName: 'John Doe'
});
expect(response.body.singleResult.data.user.posts).toBeInstanceOf(Array);
});
it('يجب أن يتعامل مع الاستعلامات غير الصالحة', async () => {
const query = gql`
query {
user(id: "999") {
id
firstName
nonExistentField
}
}
`;
const response = await server.executeOperation({ query });
expect(response.body.singleResult.errors).toBeDefined();
expect(response.body.singleResult.errors[0].message).toContain(
'Cannot query field "nonExistentField"'
);
});
it('يجب أن ينشئ مستخدمًا عبر الطفرة', async () => {
const mutation = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
success
message
user {
id
firstName
email
}
}
}
`;
const response = await server.executeOperation({
query: mutation,
variables: {
input: {
firstName: 'Bob',
lastName: 'Wilson',
email: 'bob@example.com'
}
}
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data.createUser.success).toBe(true);
expect(response.body.singleResult.data.createUser.user.email).toBe(
'bob@example.com'
);
});
});
محاكاة مخطط GraphQL
استخدم المحللات الوهمية لاختبار كود جانب العميل بدون خادم حقيقي:
const { ApolloServer } = require('@apollo/server');
const { addMocksToSchema } = require('@graphql-tools/mock');
const { makeExecutableSchema } = require('@graphql-tools/schema');
// إنشاء مخطط مع محاكاة
const schema = makeExecutableSchema({ typeDefs });
const mocks = {
Int: () => 42,
Float: () => 3.14,
String: () => 'سلسلة وهمية',
User: () => ({
id: 'mock-user-id',
firstName: 'وهمي',
lastName: 'مستخدم',
email: 'mock@example.com'
}),
Post: () => ({
id: 'mock-post-id',
title: 'عنوان المنشور الوهمي',
content: 'محتوى المنشور الوهمي'
})
};
const schemaWithMocks = addMocksToSchema({
schema,
mocks,
preserveResolvers: false // استبدل جميع المحللات بالمحاكاة
});
const server = new ApolloServer({
schema: schemaWithMocks
});
// جميع الاستعلامات تعيد بيانات وهمية
// query { user(id: "1") { firstName } }
// الإرجاع: { "data": { "user": { "firstName": "وهمي" } } }
نصيحة: استخدم
preserveResolvers: true للاحتفاظ ببعض المحللات الحقيقية أثناء محاكاة الآخرين. هذا مفيد لاختبار أجزاء معينة من المخطط الخاص بك.
اختبار الاستعلامات والطفرات
اكتب اختبارات شاملة لسيناريوهات الاستعلام المختلفة:
describe('اختبارات الاستعلام', () => {
it('يجب أن يتعامل مع الترقيم بشكل صحيح', async () => {
const query = gql`
query GetPosts($limit: Int!, $offset: Int!) {
posts(limit: $limit, offset: $offset) {
id
title
}
}
`;
const response = await server.executeOperation({
query,
variables: { limit: 5, offset: 0 }
});
expect(response.body.singleResult.data.posts).toHaveLength(5);
});
it('يجب أن يصفي النتائج بناءً على الوسائط', async () => {
const query = gql`
query SearchPosts($searchTerm: String!) {
posts(search: $searchTerm) {
id
title
}
}
`;
const response = await server.executeOperation({
query,
variables: { searchTerm: 'GraphQL' }
});
const posts = response.body.singleResult.data.posts;
posts.forEach(post => {
expect(post.title.toLowerCase()).toContain('graphql');
});
});
it('يجب أن يتعامل مع أخطاء المصادقة', async () => {
const query = gql`
query GetMe {
me {
id
email
}
}
`;
// التنفيذ بدون مصادقة
const response = await server.executeOperation({ query });
expect(response.body.singleResult.errors).toBeDefined();
expect(response.body.singleResult.errors[0].message).toContain(
'غير مصادق عليه'
);
});
});
describe('اختبارات الطفرة', () => {
it('يجب أن يتحقق من صحة بيانات الإدخال', async () => {
const mutation = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
success
message
}
}
`;
// بريد إلكتروني غير صالح
const response = await server.executeOperation({
query: mutation,
variables: {
input: {
firstName: 'اختبار',
lastName: 'مستخدم',
email: 'invalid-email'
}
}
});
expect(response.body.singleResult.data.createUser.success).toBe(false);
expect(response.body.singleResult.data.createUser.message).toContain(
'بريد إلكتروني غير صالح'
);
});
it('يجب أن يتعامل مع الطفرات المتزامنة', async () => {
const mutation = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
success
post { id }
}
}
`;
// تنفيذ طفرات متعددة بشكل متزامن
const promises = Array.from({ length: 10 }, (_, i) =>
server.executeOperation({
query: mutation,
variables: {
input: {
title: `منشور ${i}`,
content: `محتوى ${i}`
}
}
})
);
const responses = await Promise.all(promises);
responses.forEach(response => {
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data.createPost.success).toBe(true);
});
});
});
اختبار اللقطات
استخدم اختبار اللقطات للكشف عن تغييرات المخطط أو الاستجابة غير المتوقعة:
const { printSchema } = require('graphql');
const { makeExecutableSchema } = require('@graphql-tools/schema');
describe('اختبارات لقطة المخطط', () => {
it('يجب أن يطابق لقطة المخطط', () => {
const schema = makeExecutableSchema({ typeDefs, resolvers });
const schemaString = printSchema(schema);
expect(schemaString).toMatchSnapshot();
});
it('يجب أن يطابق لقطة استجابة الاستعلام', async () => {
const query = gql`
query GetUser {
user(id: "1") {
id
firstName
lastName
email
posts {
id
title
}
}
}
`;
const response = await server.executeOperation({ query });
expect(response.body.singleResult.data).toMatchSnapshot();
});
});
// التشغيل الأول ينشئ ملف لقطة
// التشغيلات اللاحقة تقارن مع اللقطة
// إذا تغير المخطط/الاستجابة، يفشل الاختبار
// قم بتشغيل `jest -u` لتحديث اللقطات
تحذير: اختبارات اللقطات مفيدة لاكتشاف التغييرات غير المقصودة، لكنها يمكن أن تصبح هشة. استخدمها بشكل انتقائي لهياكل المخطط الحرجة وراجع دائمًا تغييرات اللقطة بعناية.
أدوات الاختبار
أنشئ أدوات اختبار قابلة لإعادة الاستخدام ومساعدين:
// testUtils.js
const { ApolloServer } = require('@apollo/server');
// إنشاء خادم اختبار مع سياق وهمي
function createTestServer(options = {}) {
const {
typeDefs,
resolvers,
context = {}
} = options;
return new ApolloServer({
typeDefs,
resolvers,
context: async () => ({
user: null,
dataSources: {},
...context
})
});
}
// مساعد تنفيذ الاستعلام
async function executeQuery(server, query, variables = {}) {
const response = await server.executeOperation({
query,
variables
});
if (response.body.singleResult.errors) {
throw new Error(response.body.singleResult.errors[0].message);
}
return response.body.singleResult.data;
}
// مساعد سياق المصادقة
function authenticatedContext(userId) {
return {
user: {
id: userId,
role: 'USER'
}
};
}
// مساعد سياق المسؤول
function adminContext(userId) {
return {
user: {
id: userId,
role: 'ADMIN'
}
};
}
module.exports = {
createTestServer,
executeQuery,
authenticatedContext,
adminContext
};
// الاستخدام في الاختبارات
const { createTestServer, executeQuery, authenticatedContext } = require('./testUtils');
describe('استخدام أدوات الاختبار', () => {
it('يجب أن يجلب بيانات المستخدم مع المصادقة', async () => {
const server = createTestServer({
typeDefs,
resolvers,
context: authenticatedContext('user-123')
});
const query = gql`
query GetMe {
me {
id
email
}
}
`;
const data = await executeQuery(server, query);
expect(data.me.id).toBe('user-123');
});
});
اختبار معالجة الأخطاء
تحقق من معالجة الأخطاء وتنسيقها بشكل صحيح:
describe('اختبارات معالجة الأخطاء', () => {
it('يجب أن يُنسق أخطاء التحقق', async () => {
const mutation = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
success
message
}
}
`;
const response = await server.executeOperation({
query: mutation,
variables: {
input: {
title: '', // عنوان فارغ - خطأ في التحقق
content: 'محتوى الاختبار'
}
}
});
expect(response.body.singleResult.data.createPost.success).toBe(false);
expect(response.body.singleResult.data.createPost.message).toBe(
'العنوان مطلوب'
);
});
it('يجب أن يتعامل مع أخطاء التفويض', async () => {
const mutation = gql`
mutation DeleteUser($id: ID!) {
deleteUser(id: $id) {
success
}
}
`;
// التنفيذ بدون امتيازات المسؤول
const response = await server.executeOperation({
query: mutation,
variables: { id: 'user-123' }
});
expect(response.body.singleResult.errors).toBeDefined();
expect(response.body.singleResult.errors[0].extensions.code).toBe(
'FORBIDDEN'
);
});
});
تمرين: أنشئ مجموعة اختبار شاملة لواجهة برمجة تطبيقات مدونة GraphQL مع:
- اختبارات الوحدة لجميع محللات الاستعلام والطفرة
- اختبارات التكامل لإنشاء وتحديث وحذف المنشورات
- اختبارات للترقيم والتصفية
- اختبارات المصادقة والتفويض
- اختبارات معالجة الأخطاء للمدخلات غير الصالحة
- اختبار لقطة للمخطط الكامل