الاختبارات و TDD

اختبار واجهة برمجة التطبيقات (API)

40 دقيقة الدرس 15 من 35

مقدمة في اختبار API

يتحقق اختبار API (واجهة برمجة التطبيقات) من وظيفة وموثوقية وأداء وأمان واجهات برمجة التطبيقات. على عكس اختبار واجهة المستخدم، يركز اختبار API على طبقة منطق الأعمال واستجابات البيانات، مما يجعله أسرع وأكثر موثوقية لاختبار خدمات الخلفية.

لماذا اختبار API؟

  • السرعة: أسرع من اختبارات واجهة المستخدم، بدون عبء المتصفح
  • الموثوقية: أقل تقلباً من اختبارات واجهة المستخدم، بدون تبعيات العناصر المرئية
  • الكشف المبكر: اختبر منطق الخلفية قبل أن تكون واجهة المستخدم جاهزة
  • التغطية: اختبر الحالات الحدية وسيناريوهات الخطأ بسهولة
  • اختبار التكامل: تحقق من التواصل بين الخدمات
  • اختبار الأداء: قياس أوقات الاستجابة والإنتاجية

أنواع اختبار API

  • الاختبار الوظيفي: تحقق من أن نقاط النهاية تعيد البيانات الصحيحة
  • اختبار التكامل: اختبر التفاعلات بين واجهات برمجة تطبيقات متعددة
  • اختبار الحمل: اختبر الأداء تحت حركة مرور عالية
  • اختبار الأمان: التحقق من المصادقة والتفويض
  • اختبار العقد: تأكد من احترام عقود API
  • معالجة الأخطاء: اختبر استجابات الخطأ والحالات الحدية

اختبار API اليدوي باستخدام Postman

Postman هي أداة اختبار API شعبية مع واجهة سهلة الاستخدام:

الطلب الأساسي

# مثال: اختبار نقطة نهاية GET GET https://api.example.com/users # الرؤوس Authorization: Bearer <token> Content-Type: application/json # الاستجابة (200 OK) { "users": [ { "id": 1, "name": "John Doe", "email": "john@example.com" } ], "total": 1 }

طلب POST

# إنشاء مستخدم جديد POST https://api.example.com/users # الجسم (JSON) { "name": "Jane Smith", "email": "jane@example.com", "password": "securepass123" } # الاستجابة (201 Created) { "id": 2, "name": "Jane Smith", "email": "jane@example.com", "created_at": "2026-02-14T10:30:00Z" }

اختبارات Postman

أضف نصوص اختبار في علامة التبويب Tests في Postman:

// اختبار رمز الحالة pm.test("Status code is 200", function () { pm.response.to.have.status(200); }); // اختبار وقت الاستجابة pm.test("Response time is less than 500ms", function () { pm.expect(pm.response.responseTime).to.be.below(500); }); // اختبار جسم الاستجابة pm.test("Response has users array", function () { const jsonData = pm.response.json(); pm.expect(jsonData).to.have.property('users'); pm.expect(jsonData.users).to.be.an('array'); }); // اختبار بيانات محددة pm.test("First user has correct structure", function () { const jsonData = pm.response.json(); const firstUser = jsonData.users[0]; pm.expect(firstUser).to.have.property('id'); pm.expect(firstUser).to.have.property('name'); pm.expect(firstUser).to.have.property('email'); }); // حفظ البيانات للطلب التالي pm.environment.set("userId", pm.response.json().users[0].id);
نصيحة: استخدم مجموعات Postman لتنظيم طلبات API ذات الصلة ومشاركتها مع فريقك. يمكن تصدير المجموعات والتحكم في إصداراتها.

اختبار API الآلي باستخدام Jest/Vitest

أتمتة اختبار API باستخدام أطر اختبار JavaScript:

الإعداد

# تثبيت التبعيات npm install --save-dev jest node-fetch # أو استخدم axios npm install --save-dev jest axios

اختبار API الأساسي

// api.test.js const axios = require('axios'); const BASE_URL = 'https://api.example.com'; describe('User API', () => { let authToken; beforeAll(async () => { // تسجيل الدخول والحصول على رمز المصادقة const response = await axios.post(`${BASE_URL}/auth/login`, { email: 'test@example.com', password: 'testpass123' }); authToken = response.data.token; }); test('GET /users returns list of users', async () => { const response = await axios.get(`${BASE_URL}/users`, { headers: { Authorization: `Bearer ${authToken}` } }); expect(response.status).toBe(200); expect(response.data).toHaveProperty('users'); expect(Array.isArray(response.data.users)).toBe(true); }); test('GET /users/:id returns single user', async () => { const userId = 1; const response = await axios.get(`${BASE_URL}/users/${userId}`, { headers: { Authorization: `Bearer ${authToken}` } }); expect(response.status).toBe(200); expect(response.data).toHaveProperty('id', userId); expect(response.data).toHaveProperty('name'); expect(response.data).toHaveProperty('email'); }); test('POST /users creates new user', async () => { const newUser = { name: 'Test User', email: 'newuser@example.com', password: 'password123' }; const response = await axios.post(`${BASE_URL}/users`, newUser, { headers: { Authorization: `Bearer ${authToken}` } }); expect(response.status).toBe(201); expect(response.data).toHaveProperty('id'); expect(response.data.name).toBe(newUser.name); expect(response.data.email).toBe(newUser.email); expect(response.data).not.toHaveProperty('password'); // لا يجب إرجاع كلمة المرور }); test('PUT /users/:id updates user', async () => { const userId = 2; const updates = { name: 'Updated Name' }; const response = await axios.put(`${BASE_URL}/users/${userId}`, updates, { headers: { Authorization: `Bearer ${authToken}` } }); expect(response.status).toBe(200); expect(response.data.id).toBe(userId); expect(response.data.name).toBe(updates.name); }); test('DELETE /users/:id deletes user', async () => { const userId = 2; const response = await axios.delete(`${BASE_URL}/users/${userId}`, { headers: { Authorization: `Bearer ${authToken}` } }); expect(response.status).toBe(204); // تحقق من حذف المستخدم try { await axios.get(`${BASE_URL}/users/${userId}`, { headers: { Authorization: `Bearer ${authToken}` } }); fail('Expected 404 error'); } catch (error) { expect(error.response.status).toBe(404); } }); });

اختبار استجابات الخطأ

اختبارات معالجة الأخطاء الشاملة:

describe('Error Handling', () => { test('returns 400 for invalid data', async () => { try { await axios.post(`${BASE_URL}/users`, { name: '', // غير صالح: اسم فارغ email: 'invalid-email' // تنسيق غير صالح }, { headers: { Authorization: `Bearer ${authToken}` } }); fail('Expected validation error'); } catch (error) { expect(error.response.status).toBe(400); expect(error.response.data).toHaveProperty('errors'); expect(error.response.data.errors).toContainEqual( expect.objectContaining({ field: 'name' }) ); expect(error.response.data.errors).toContainEqual( expect.objectContaining({ field: 'email' }) ); } }); test('returns 401 for unauthorized requests', async () => { try { await axios.get(`${BASE_URL}/users`); // لا يوجد رمز مصادقة fail('Expected unauthorized error'); } catch (error) { expect(error.response.status).toBe(401); expect(error.response.data).toHaveProperty('message'); } }); test('returns 404 for non-existent resource', async () => { try { await axios.get(`${BASE_URL}/users/99999`, { headers: { Authorization: `Bearer ${authToken}` } }); fail('Expected not found error'); } catch (error) { expect(error.response.status).toBe(404); } }); test('returns 429 for rate limiting', async () => { // قم بعمل العديد من الطلبات السريعة const requests = Array.from({ length: 100 }, (_, i) => axios.get(`${BASE_URL}/users`, { headers: { Authorization: `Bearer ${authToken}` } }).catch(err => err.response) ); const responses = await Promise.all(requests); // يجب أن يكون واحد على الأقل محدود المعدل const rateLimited = responses.some(r => r.status === 429); expect(rateLimited).toBe(true); }); });

الاختبار باستخدام SuperTest (تطبيقات Express)

SuperTest مثالي لاختبار واجهات برمجة تطبيقات Node.js/Express:

// تثبيت npm install --save-dev supertest // api.test.js const request = require('supertest'); const app = require('../app'); // تطبيق Express الخاص بك describe('User API', () => { test('GET /users returns 200', async () => { const response = await request(app) .get('/api/users') .set('Authorization', 'Bearer token') .expect(200) .expect('Content-Type', /json/); expect(response.body).toHaveProperty('users'); expect(Array.isArray(response.body.users)).toBe(true); }); test('POST /users creates user', async () => { const newUser = { name: 'Test User', email: 'test@example.com' }; const response = await request(app) .post('/api/users') .send(newUser) .set('Authorization', 'Bearer token') .expect(201) .expect('Content-Type', /json/); expect(response.body).toMatchObject(newUser); expect(response.body).toHaveProperty('id'); }); test('POST /users validates required fields', async () => { await request(app) .post('/api/users') .send({ name: 'Test' }) // البريد الإلكتروني مفقود .set('Authorization', 'Bearer token') .expect(400); }); });

محاكاة واجهات برمجة التطبيقات الخارجية

استخدم مكتبات المحاكاة للاختبار بدون استدعاءات API حقيقية:

// تثبيت npm install --save-dev nock // استخدام Nock لمحاكاة طلبات HTTP const nock = require('nock'); describe('External API Integration', () => { afterEach(() => { nock.cleanAll(); // مسح جميع المحاكيات }); test('fetches data from external API', async () => { // محاكاة API خارجية nock('https://external-api.com') .get('/data') .reply(200, { result: 'success', data: [1, 2, 3] }); const response = await axios.get('https://external-api.com/data'); expect(response.status).toBe(200); expect(response.data.result).toBe('success'); }); test('handles external API errors', async () => { // محاكاة استجابة الخطأ nock('https://external-api.com') .get('/data') .reply(500, { error: 'Internal Server Error' }); try { await axios.get('https://external-api.com/data'); fail('Expected error'); } catch (error) { expect(error.response.status).toBe(500); } }); test('handles network timeout', async () => { nock('https://external-api.com') .get('/data') .delayConnection(10000) // تأخير 10 ثوانٍ .reply(200); try { await axios.get('https://external-api.com/data', { timeout: 1000 // مهلة ثانية واحدة }); fail('Expected timeout error'); } catch (error) { expect(error.code).toBe('ECONNABORTED'); } }); });
ملاحظة: محاكاة واجهات برمجة التطبيقات الخارجية تجعل الاختبارات أسرع وأكثر موثوقية وتمنع الوصول إلى حدود المعدل أثناء الاختبار.

اختبار العقد باستخدام Pact

يضمن اختبار العقد أن موفري API والمستهلكين يوافقون على الواجهة:

// تثبيت Pact npm install --save-dev @pact-foundation/pact // اختبار المستهلك (الواجهة الأمامية) const { Pact } = require('@pact-foundation/pact'); const path = require('path'); const provider = new Pact({ consumer: 'FrontendApp', provider: 'UserAPI', port: 8080, log: path.resolve(process.cwd(), 'logs', 'pact.log'), dir: path.resolve(process.cwd(), 'pacts') }); describe('User API Contract', () => { beforeAll(() => provider.setup()); afterAll(() => provider.finalize()); test('get user by ID', async () => { // تحديد التفاعل المتوقع await provider.addInteraction({ state: 'user with ID 1 exists', uponReceiving: 'a request for user 1', withRequest: { method: 'GET', path: '/users/1', headers: { Accept: 'application/json' } }, willRespondWith: { status: 200, headers: { 'Content-Type': 'application/json' }, body: { id: 1, name: 'John Doe', email: 'john@example.com' } } }); // قم بعمل طلب فعلي const response = await axios.get('http://localhost:8080/users/1', { headers: { Accept: 'application/json' } }); // تحقق من أن الاستجابة تطابق العقد expect(response.data).toMatchObject({ id: 1, name: 'John Doe', email: 'john@example.com' }); await provider.verify(); }); });

اختبار الأداء

قياس أداء API وتحديد الاختناقات:

describe('Performance Tests', () => { test('response time is under 200ms', async () => { const start = Date.now(); await axios.get(`${BASE_URL}/users`, { headers: { Authorization: `Bearer ${authToken}` } }); const duration = Date.now() - start; expect(duration).toBeLessThan(200); }); test('handles concurrent requests', async () => { const requests = Array.from({ length: 50 }, () => axios.get(`${BASE_URL}/users`, { headers: { Authorization: `Bearer ${authToken}` } }) ); const start = Date.now(); const responses = await Promise.all(requests); const duration = Date.now() - start; // يجب أن تنجح جميع الطلبات responses.forEach(response => { expect(response.status).toBe(200); }); // يجب أن يكون متوسط وقت الاستجابة معقولاً const avgTime = duration / requests.length; expect(avgTime).toBeLessThan(500); }); });

اختبار الأمان

اختبر المصادقة والتفويض والتحقق من صحة المدخلات:

describe('Security Tests', () => { test('requires authentication', async () => { try { await axios.get(`${BASE_URL}/users`); fail('Expected authentication error'); } catch (error) { expect(error.response.status).toBe(401); } }); test('validates JWT token', async () => { try { await axios.get(`${BASE_URL}/users`, { headers: { Authorization: 'Bearer invalid-token' } }); fail('Expected invalid token error'); } catch (error) { expect(error.response.status).toBe(401); } }); test('prevents SQL injection', async () => { const maliciousInput = "1' OR '1'='1"; try { const response = await axios.get(`${BASE_URL}/users/${maliciousInput}`, { headers: { Authorization: `Bearer ${authToken}` } }); // يجب إرجاع 404، وليس جميع المستخدمين expect(response.status).toBe(404); } catch (error) { expect(error.response.status).toBe(404); } }); test('sanitizes user input', async () => { const xssPayload = '<script>alert("XSS")</script>'; const response = await axios.post(`${BASE_URL}/users`, { name: xssPayload, email: 'test@example.com', password: 'password123' }, { headers: { Authorization: `Bearer ${authToken}` } }); // يجب تطهير/تجاوز حمولة XSS expect(response.data.name).not.toContain('<script>'); }); test('enforces rate limiting', async () => { const requests = Array.from({ length: 100 }, () => axios.get(`${BASE_URL}/users`, { headers: { Authorization: `Bearer ${authToken}` } }).catch(err => err.response) ); const responses = await Promise.all(requests); const rateLimited = responses.filter(r => r.status === 429); expect(rateLimited.length).toBeGreaterThan(0); }); });
تمرين:

أنشئ مجموعة اختبارات API شاملة لـ REST API للمدونة:

  1. اختبر نقاط نهاية المصادقة (تسجيل الدخول، تسجيل الخروج، تحديث الرمز)
  2. اختبر عمليات CRUD لمنشور المدونة (إنشاء، قراءة، تحديث، حذف)
  3. اختبر API التعليقات (إنشاء، قراءة، تحديث، حذف التعليقات)
  4. اختبر الترقيم والترشيح (معلمات الاستعلام)
  5. اختبر التفويض (يمكن للمستخدمين تحرير منشوراتهم فقط)
  6. اختبر التحقق من الصحة (الحقول المطلوبة، تنسيق البريد الإلكتروني، إلخ)
  7. اختبر استجابات الخطأ (400، 401، 403، 404، 500)
  8. اختبر رفع الملف (صور منشور المدونة)
  9. قياس أوقات الاستجابة لجميع نقاط النهاية
  10. محاكاة الخدمات الخارجية (API معالجة الصور)

استخدم هيكل اختبار مناسب مع الإعداد/التفكيك، وأنشئ تركيبات بيانات الاختبار، ونظم الاختبارات حسب المورد.

أفضل الممارسات

  • اختبر المسارات السعيدة والحالات الحدية: غطِّ سيناريوهات النجاح وشروط الخطأ
  • استخدم رموز حالة HTTP المناسبة: تحقق من إرجاع رموز الحالة الصحيحة
  • اختبر المصادقة/التفويض: تحقق من أن عناصر التحكم الأمنية تعمل بشكل صحيح
  • محاكاة التبعيات الخارجية: لا تعتمد على الخدمات الخارجية في الاختبارات
  • اختبر التحقق من صحة البيانات: تأكد من أن التحقق من صحة المدخلات يعمل بشكل صحيح
  • نظف بيانات الاختبار: احذف الموارد المنشأة بعد الاختبارات
  • استخدم متغيرات البيئة: قم بتكوين عناوين URL والبيانات الاعتمادية لـ API خارجياً
  • نظم الاختبارات حسب المورد: جمّع اختبارات نقطة النهاية ذات الصلة معاً
أخطاء شائعة:
  • اختبار المسارات السعيدة فقط، تجاهل سيناريوهات الخطأ
  • عدم تنظيف بيانات الاختبار (تلوث قاعدة البيانات)
  • تشفير عناوين URL والبيانات الاعتمادية لـ API في الاختبارات
  • عدم اختبار الحالات الحدية (المصفوفات الفارغة، القيم الخالية، إلخ)
  • تخطي اختبارات المصادقة/التفويض
  • عدم التحقق من بنية الاستجابة وأنواع البيانات
  • إنشاء اختبارات تابعة يجب تشغيلها بترتيب محدد

الخلاصة

اختبار API حاسم لضمان خدمات خلفية موثوقة. استخدم أدوات يدوية مثل Postman للاستكشاف والاختبارات الآلية مع Jest/SuperTest للتكامل المستمر. اجمع بين الاختبار الوظيفي والأمان والأداء واختبار العقد للحصول على تغطية شاملة.

هذا يختم سلسلة دروس الاختبار و TDD. لديك الآن المعرفة لكتابة اختبارات الوحدة واختبارات التكامل واختبارات المكونات واختبارات E2E واختبارات API لتطبيقات الويب الحديثة.