أتمتة اختبارات API
أتمتة اختبارات API تحوّل اختبارات الواجهات البرمجية إلى شيفرة أو إلى مجموعات طلبات قابلة للتشغيل: فترسل الآلة الطلبات، وتقيّم التأكيدات (Assertions)، وتعلن نتيجة كل اختبار ناجحاً أو فاشلاً، دون أن يكرر أحد الخطوات بيده. ويوفّر إطار الاختبار (Test Framework) الأجزاء اللازمة: مشغّل اختبارات (Test Runner) يكتشف الاختبارات ويشغّلها، وعميل HTTP، والتأكيدات، وخطافات (Hooks) للتهيئة والتنظيف، وأدوات تقارير (Reporters) تكتب النتائج.
خذ قائمة الفحص الخاصة بـPOST /api/v1/orders من درس التحقق من صحة استجابات API. بعد أتمتتها تصبح اختباراً ينشئ طلبية، ويتحقق من 201 وترويسة Location والحالة pending والمجموع المحسوب، ثم يقرأ الطلبية من جديد. وفي خط أنابيب CI يعمل هذا الاختبار مع كل طلب دمج (Pull Request)، وإذا فشل أوقف دمج التغيير حتى ينظر فيه أحد.
لماذا يهم
تتغير الواجهات البرمجية كثيراً، وكل تغيير قد يُعطّل عميلاً ما في مكان ما: حقل أُعيدت تسميته، أو قاعدة تحقق توقفت عن العمل، أو 200 حيث وُعد بـ201. وتكرار عشرات فحوص الطلبات يدوياً بعد كل دمج بطيء، وعملياً لا يحدث. أما حزمة الانحدار المؤتمتة فتكرر هذه الفحوص مع كل تغيير، فيُكتشف العيب في تشغيل خط الأنابيب نفسه الذي جاء مع الإيداع (Commit) المسبّب له، بينما التغيير ما زال حاضراً في ذهن المطور.
واختبارات API من أنفع الاختبارات المؤتمتة أيضاً: فهي تتجاوز واجهة المستخدم، لذا تعمل أسرع وتتعطل أقل من اختبارات الواجهة الرسومية، وتصل إلى قواعد العمل ومسارات الخطأ التي لا تستطيع أي شاشة أن تستدعيها. وبالنسبة إلى المختبِر، تنقل الأتمتة الجهد من إعادة تشغيل الفحوص إلى تصميمها. لكن ذلك لا ينجح إلا ما دام الفريق يثق بالحزمة: فالاختبارات التي تفشل لأسباب لا علاقة لها بالمنتج، مثل البيانات المشتركة أو عنوان URL مكتوب داخل الشيفرة، سرعان ما تُتجاهَل.
أطر العمل: ماذا تقدّم وكيف تختار
يجمع كل إطار لأتمتة اختبارات API، أياً كانت لغته، الأجزاء نفسها: مشغّل اختبارات يكتشف الاختبارات ويشغّلها، وغالباً على التوازي، ويعلن النتائج؛ وعميل HTTP يرسل الطلبات ويعرض رمز الحالة والترويسات والجسم؛ والتأكيدات؛ وخطافات أو تجهيزات (Fixtures) تُعِدّ ما يلزم قبل الاختبارات وتنظّف بعدها؛ وأدوات تقارير، منها JUnit XML، وهي صيغة النتائج التي تقرؤها معظم أنظمة CI. والفرق الأساسي بين الأطر هو طريقة كتابة الاختبارات، ولا يصمّم أي منها الاختبارات بدلاً منك: فالتقنيات والنتائج المتوقعة ما زالت تأتي من الدروس السابقة.
- المبنية على الشيفرة: الاختبارات ملفات بلغة برمجة. ومن أمثلتها Playwright Test مع التجهيزة
request(بلغة TypeScript أو JavaScript)، وREST Assured مع JUnit أو TestNG (بلغة Java)، وpytest مع مكتبةrequests(بلغة Python). وتمنحك تحكماً كاملاً في البيانات وإعادة الاستخدام والبنية، وتُراجَع الاختبارات كأي شيفرة أخرى. - المبنية على المجموعات: طلبات محفوظة في عميل API مع نصوص اختبار، مثل مجموعة (Collection) في Postman. وتعمل المجموعة نفسها من سطر الأوامر عبر Newman أو Postman CLI، وبهذا تصل إلى CI. البدء بها سريع، لكن مشاركة الدوال المساعدة ومراجعة التغييرات تصبحان أصعب كلما كبرت المجموعة.
- اختر ما يستخدمه مطورو فريقك أصلاً، لتعيش الاختبارات في المستودع نفسه ويستطيع أي فرد تشغيلها وإصلاحها. والمفاهيم في هذا الدرس تنتقل من إطار إلى آخر.
مدخل مختصر إلى شيفرة اختبارات API
يمكنك قراءة اختبارات API البسيطة وكتابتها دون أن تكون مطوراً. فأنت تحتاج إلى القيم والكائنات والحلقات والدوال من درس «قراءة الشيفرة للمختبِرين»، وإلى بضع ميزات أخرى يستخدمها كل اختبار API. والشيفرة في آخر هذا القسم ملف اختبار كامل بلغة TypeScript لإطار Playwright Test، ما زال بلا طبقات: اختبار إيجابي واحد واختباران سلبيان معتمدان على البيانات لـPOST /api/v1/orders، الذي يقبل qty من 1 إلى 99. والملاحظات أدناه تقرؤه جزءاً جزءاً. وهو لا يحذف الطلبيات التي ينشئها؛ فالأقسام التالية تضيف الطبقات والتنظيف. وللتعمق لاحقاً، يشرح درس «أساسيات البرمجة لأتمتة الاختبار» الوحدات البرمجية والأصناف والشيفرة غير المتزامنة بالتفصيل؛ وهو قراءة إضافية وليس شرطاً لهذا الدرس.
- بنية ملف الاختبار: ملف الاختبار وحدة برمجية (Module). فالسطر
import { test, expect } from '@playwright/test'يأخذ دالتين من حزمة الإطار:testتعرّف اختباراً، وexpectتكتب تأكيداً. ويشارك الملف دواله مع غيره بالكلمةexport، وتأخذها الملفات الأخرى بـimport، كما تفعل الطبقات أدناه. ويبحث المشغّل في مجلد الاختبارات عن الملفات التي تنتهي أسماؤها بـ.spec.tsأو.test.ts، ويشغّل كلtestفيها. - الاختبار:
test('creates a pending order', async ({ request }) => { … })يعطي الاختبار عنواناً يظهر في التقرير، ودالةً يستدعيها المشغّل. والرمز=>طريقة مختصرة لكتابة دالة (Arrow Function)، و{ request }تأخذ التجهيزةrequestالتي يمرّرها المشغّل: عميل HTTP مهيّأ مسبقاً بعنوان الأساس والترويسات من الإعدادات. - async وawait: يستغرق الطلب وقتاً، لذا تعيد
request.post(…)وعداً (Promise)، أي قيمة تصل لاحقاً. والكلمةawaitتنتظره قبل أن يعمل السطر التالي، ولا يجوز استخدامawaitإلا في دالة موسومة بـasync. ونسيانawaitهو الخطأ الشائع لدى المبتدئين: يتعامل السطر التالي مع وعد لم يكتمل بدلاً من الاستجابة، فيفشل الاختبار برسالة مربكة، أو ينتهي قبل اكتمال الطلب. - استدعاء الطلب:
request.post(path, { data })يرسلPOSTإلى المسار، ويُحسب المسار نسبةً إلى عنوان الأساس. ويحوّل Playwright الكائنdataإلى جسم JSON، ويضبطContent-Type: application/jsonما لم تضبطه بنفسك. وتعملrequest.getوputوpatchوdeleteبالطريقة نفسها. - التأكيدات على الحالة والترويسات والجسم:
expect(actual).toBe(expected)تقارن مقارنةً دقيقة. وres.status()هو رمز الحالة، وres.headers()تعيد الترويسات بأسماء بأحرف صغيرة، وawait res.json()تحلّل الجسم، ثم تقرأ حقوله كأي كائن (order.status). وtoContainتفحص جزءاً من نص، وtoHaveLengthطول قائمة. وأولexpectيفشل يوقف الاختبار ويعرض القيمة المتوقعة والقيمة الفعلية. - الصفوف المعتمدة على البيانات:
for (const qty of [0, 100])تسجّل اختباراً لكل قيمة. والعنوان قالب نصي (Template Literal)، أي نص بين علامتَي الاقتباس الخلفيتين (Backtick) تُدرج فيه${qty}القيمة، فيعرض التقرير «rejects qty 0» و«rejects qty 100» اختبارين منفصلين. - إعدادات البيئة: لا يحتوي الملف على اسم خادم ولا على مفتاح. فعنوان الأساس ورمز الوصول يأتيان من متغيرات البيئة (Environment Variables)، وهي قيم مسمّاة تُضبط خارج الشيفرة في الطرفية أو في مهمة CI، وتُقرأ في الشيفرة هكذا:
process.env.API_BASE_URL. ويقرؤها ملف الإعدادات في قسم البيئات أدناه مرة واحدة، فيعمل الاختبار نفسه على البيئة المحلية أو test أو staging، ولا يُودَع أي سر في المستودع.
// tests/orders/first.spec.ts
import { test, expect } from '@playwright/test';
test('creates a pending order', async ({ request }) => {
const res = await request.post('/api/v1/orders', {
data: { customerId: 7, items: [{ sku: 'A-100', qty: 2 }] },
});
expect(res.status()).toBe(201);
expect(res.headers()['content-type']).toContain('application/json');
const order = await res.json();
expect(order.status).toBe('pending');
expect(order.items).toHaveLength(1);
});
for (const qty of [0, 100]) {
test(`rejects qty ${qty}`, async ({ request }) => {
const res = await request.post('/api/v1/orders', {
data: { customerId: 7, items: [{ sku: 'A-100', qty: qty }] },
});
expect(res.status()).toBe(422);
});
}بنية الحزمة في طبقات
خمسة اختبارات يمكن أن تعيش في ملف واحد، أما خمسمئة فلا: فإذا بنى كل اختبار عناوينه ورموز الوصول وJSON الخاص به، فإن إعادة تسمية مسار واحد تعني تعديل كل اختبار يستدعيه. والحزمة القابلة للصيانة تقسّم العمل إلى طبقات، لكل منها سبب واحد للتغيير، ولا تصل الاختبارات إلى الواجهة إلا عبر الطبقات التي تحتها. ويسمي بعض الفرق طبقة العميل «طبقة الخدمة» (Service Layer).
- الاختبارات (
tests/): سلوك واحد في كل اختبار، يُسمّى الاختبار باسمه، وتُجمع الاختبارات حسب المورد. يُعِدّ الاختبار مدخله ويستدعي العميل ويتحقق من النتيجة، ولا يحتوي على عناوين URL أو رموز وصول (Tokens) أو طلبات خام. - عميل API (
api/): دالة واحدة لكل نقطة نهاية (Endpoint)، مثلcreate()وget(id)للطلبيات. المسارات والطرق والترويسات الافتراضية موجودة هنا فقط، فيُصلَح المسار المتغيّر مرة واحدة. ويعيد العميل الاستجابة ولا يتحقق منها؛ فالاختبار هو من يقرر ما الصحيح. - الإعدادات (
config/): عنوان الأساس (Base URL) وبيانات الاعتماد والمهل والسجلات للبيئة التي يستهدفها التشغيل، تُقرأ من متغيرات البيئة، ويُهيَّأ منها عميل HTTP الخاص بالإطار مرة واحدة. - بيانات الاختبار (
data/): مُنشئات (Builders) تعيد جسم طلب صالحاً افتراضياً لا يغيّر منه الاختبار إلا ما يهمه، مع خطافات تهيئة وتنظيف للسجلات التي يحتاجها الاختبار. - الفحوص المشتركة (
schemas/): مخططات JSON Schema وتأكيدات قابلة لإعادة الاستخدام، حتى يفحص كل اختبار يستلم طلبيةً شكلَها بالطريقة نفسها.
الاستدعاءات تنزل عبر الطبقات
- 1الاختباراتسلوك واحد في كل اختبار: المدخل والاستدعاء والتأكيدات؛ والاختبار هو من يقرر ما الصحيح
تتغير حين يتغير المتطلب
- 2عميل APIدالة لكل نقطة نهاية، مثل
create()وget(id): المسارات والطرق والترويسات الافتراضية؛ بلا تأكيداتتتغير حين يتغير مسار أو معامل: ملف واحد لا كل الاختبارات
- 3عميل HTTP والإعداداتعنوان الأساس ورمز الوصول والمهل والسجلات، تُقرأ من متغيرات البيئة
تتغير بتغير البيئة، ولا تُكتب في شيفرة الاختبار
- 4الواجهة قيد الاختبارفي بيئة واحدة: محلية أو test أو staging؛ ولا يُوصل إليها إلا عبر الطبقات التي فوقها
من الاختبارات إلى عميل HTTP والإعدادات:غير مسموح: عنوان URL أو رمز وصول أو طلب خام داخل اختبار
بجانب مسار الاستدعاءات: المُنشئات والخطافات في data/ تُنشئ ما يحتاجه كل اختبار وتحذفه، ومخططات JSON Schema في schemas/ تتشاركها الاختبارات، والمشغّل يكتب تقرير JUnit في results/ من أجل CI.
tests/
orders/
create-order.spec.ts one behaviour per test
order-validation.spec.ts data-driven invalid orders
customers/
api/
orders-client.ts create(), get(), remove(): paths live here
data/
order-builder.ts newOrder(): valid defaults, change what matters
schemas/
order.schema.json the shape of an order, shared by tests
config/
env.ts required(): stops when a variable is missing
playwright.config.ts runner, reporters, HTTP defaults
.github/workflows/api-tests.ymlالتأكيدات وبيانات الاختبار والبيئات
ثلاثة قرارات تصنع الفرق بين حزمة يثق بها الفريق وحزمة يتجاهلها. وكل منها يطبّق ما تعرفه من اختبار API اليدوي، مع قواعد تصبح مهمة حين لا يراقب أحد التشغيل:
- التأكيدات: تحقق من رمز الحالة بالضبط أولاً، وأرفق جسم الاستجابة برسالة الفشل ليشرح التقرير نفسه. ثم تحقق مما وُجد الاختبار من أجله: قيم دقيقة لما أرسلته، والنوع والصيغة للقيم المولَّدة مثل
idوcreatedAt، وقاعدة العمل. وافحص البنية بمخطط JSON Schema مشترك بدلاً من تكرار فحوص الحقول في كل اختبار. - تجنّب مقارنة الاستجابة كاملةً بنسخة محفوظة: فكل حقل جديد غير ضار يكسر الاختبار، والنسخة تحتفظ بأي عيب كان فيها لحظة تسجيلها.
- البيانات: ينشئ كل اختبار ما يحتاجه، عبر الواجهة أو سكربت بذر (Seed)، بقيم فريدة مثل معرّف التشغيل في الأسماء والرموز، حتى تعمل الاختبارات منفردة وبأي ترتيب وعلى التوازي. ويجوز مشاركة البيانات المرجعية للقراءة فقط، مثل عميل اختبار تُدرجه بيانات البذر مسبقاً، أما البيانات التي يغيّرها اختبار فلا.
- التنظيف: احذف ما أنشأه الاختبار في خطاف لاحق (After-hook) يعمل حتى إن فشل الاختبار، أو شغّل الحزمة على بيئة يُعاد بناؤها لكل تشغيل.
- الاختبارات المعتمدة على البيانات (Data-driven): الفئات غير الصالحة والقيم الحدّية من درس اختبار API السلبي تصبح صفوفاً في جدول واحد، لكل صف مدخله ونتيجته المتوقعة، ويشغّلها جسم اختبار واحد.
- البيئات: عنوان الأساس وبيانات الاعتماد والمهل تأتي من الإعدادات لا من شيفرة الاختبار، فتعمل الحزمة نفسها على حاسوب المطور وفي CI ومقابل staging. والأسرار تأتي من مخزن الأسرار في CI أو من ملف محلي لا يُودَع في المستودع أبداً. والاختبارات التي تنشئ بيانات أو تحذفها لا تعمل مقابل بيئة الإنتاج أبداً. وتعرض الشيفرة أدناه طبقة الإعدادات: دالة مساعدة توقف التشغيل إذا غاب متغير، وإعدادات Playwright التي تستخدمها.
// config/env.ts: stop at once when the environment is not configured
export function required(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`Missing environment variable ${name}`);
return value;
}
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import { required } from './config/env';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
reporter: [['list'], ['junit', { outputFile: 'results/api-junit.xml' }]],
use: {
baseURL: required('API_BASE_URL'), // local, test or staging
extraHTTPHeaders: { Authorization: `Bearer ${required('API_TOKEN')}` },
},
});مثال تطبيقي: أتمتة فحوص الطلبية الجديدة
عقد POST /api/v1/orders هو نفسه في درس التحقق من صحة استجابات API: 201 Created، وترويسة Location بمسار الطلبية، والحالة pending، وtotal يساوي مجموع qty × unitPrice. ويضيف العقد قواعد تحقق: للطلبية عنصر واحد على الأقل، وqty عدد صحيح من 1 إلى 99، وكل sku يجب أن يكون موجوداً؛ والطلبية غير الصالحة تعيد 422 مع مصفوفة errors تسمّي قيمُ field فيها كلَّ حقل غير صالح، مثل items[0].qty. وبكتابتها في Playwright Test وفق الطبقات السابقة، تعرض الشيفرة في آخر هذا القسم عميل الطلبيات ومُنشئ البيانات وملفَّي اختبار فيهما اختبار إيجابي واحد وخمسة اختبارات سلبية معتمدة على البيانات. والملاحظات أدناه تشرح ما تفعله:
- كل اختبار يفحص سلوكاً واحداً، وعنوانه يذكره، فيسمّي التقرير القاعدةَ المكسورة مباشرةً. والوسيط الثاني في
expect(res.status(), await res.text())هو رسالة الفشل، فإذا جاء رمز حالة خاطئ ظهر في التقرير مع جسم الاستجابة. وtoMatchObjectتنجح إذا احتوت الطلبية على الحقول المذكورة بهذه القيم، وتتجاهل حقولها الأخرى. - القيم المولَّدة تُفحص بعلاقاتها: يجب أن يساوي
Locationمسار الطلبية المبني منidالجديد، والمجموع المتوقع يُحسب من عناصر الاستجابة ويُقارن رقماً بدقة منزلتين عشريتين. - قراءة الطلبية من جديد بـ
GETتثبت أنها خُزّنت، لا أنها أُعيدت كما أُرسلت فقط. - الصفوف السلبية تتشارك جسم اختبار واحداً؛ فإضافة قيمة حدّية جديدة سطر واحد، ويظهر كل صف في التقرير اختباراً مستقلاً. والصفان
qty 0وqty 100يقعان خارج النطاق من 1 إلى 99 مباشرةً. وerrors.map(…)تحوّل قائمة كائنات الأخطاء إلى قائمة بقيمfieldفيها، وtoContainتتحقق من أن الحقل المتوقع من بينها. - الطبقات:
OrdersApiصنف (Class)، أي قالب لإنشاء الكائنات. فالتعبيرnew OrdersApi(request)ينشئ عميل طلبيات يحتفظ بالتجهيزةrequest، ودوالهcreate()وget()وremove()تحمل المسارات وتعيد الاستجابة دون أن تتحقق منها. أماnewOrder()فتعيد طلبية صالحة للعميل (Customer) رقم 7، وهو عميل اختبار تُدرجه بيانات البذر ولا يغيّره أي اختبار، و...changesتنسخ الحقول التي يمرّرها الاختبار فوق هذه القيم الافتراضية. - التنظيف: يسجّل كل اختبار معرّفات الطلبيات التي ينشئها، ويحذفها
test.afterEach، وهو خطاف يعمل بعد كل اختبار في الملف حتى إن فشل، فتعمل الاختبارات منفردة وبأي ترتيب وعلى التوازي. وفي الحزم الأكبر تُنقل هذه الخطافات إلى جانب المُنشئات فيdata/لتتشاركها ملفات الاختبار كلها.
// api/orders-client.ts: the only file that knows the order paths
import type { APIRequestContext } from '@playwright/test';
export class OrdersApi {
constructor(private request: APIRequestContext) {}
create(order: object) { return this.request.post('/api/v1/orders', { data: order }); }
get(id: number) { return this.request.get(`/api/v1/orders/${id}`); }
remove(id: number) { return this.request.delete(`/api/v1/orders/${id}`); }
}
// data/order-builder.ts: a valid order; a test changes only what it is about
export function newOrder(changes: object = {}) {
return { customerId: 7, items: [{ sku: 'A-100', qty: 1 }], ...changes };
}
// tests/orders/create-order.spec.ts
import { test, expect } from '@playwright/test';
import { OrdersApi } from '../../api/orders-client';
import { newOrder } from '../../data/order-builder';
let created: number[] = []; // ids of the orders this test created
test.afterEach(async ({ request }) => { // runs after every test, even a failed one
for (const id of created) await new OrdersApi(request).remove(id);
created = [];
});
test('creates a pending order with the correct total', async ({ request }) => {
const orders = new OrdersApi(request);
const input = newOrder({
items: [{ sku: 'A-100', qty: 2 }, { sku: 'B-200', qty: 1 }],
});
const res = await orders.create(input);
expect(res.status(), await res.text()).toBe(201); // body shown on failure
const order = await res.json();
created.push(order.id); // for the after-hook
expect(res.headers()['location']).toBe(`/api/v1/orders/${order.id}`);
expect(order).toMatchObject({ customerId: input.customerId, status: 'pending' });
let sum = 0;
for (const item of order.items) sum = sum + item.qty * item.unitPrice;
expect(order.total).toBeCloseTo(sum, 2);
const stored = await orders.get(order.id); // stored, not only echoed
expect(stored.status()).toBe(200);
expect(await stored.json()).toMatchObject({ id: order.id, total: order.total });
});
// tests/orders/order-validation.spec.ts (same imports)
const invalid = [
{ name: 'qty 0', items: [{ sku: 'A-100', qty: 0 }], field: 'items[0].qty' },
{ name: 'qty 100', items: [{ sku: 'A-100', qty: 100 }], field: 'items[0].qty' },
{ name: 'qty as text', items: [{ sku: 'A-100', qty: '2' }], field: 'items[0].qty' },
{ name: 'unknown sku', items: [{ sku: 'Z-999', qty: 1 }], field: 'items[0].sku' },
{ name: 'no items', items: [], field: 'items' },
];
for (const row of invalid) {
test(`rejects an order with ${row.name}`, async ({ request }) => {
const res = await new OrdersApi(request).create(newOrder({ items: row.items }));
expect(res.status(), await res.text()).toBe(422);
const fields = (await res.json()).errors.map((e: { field: string }) => e.field);
expect(fields).toContain(row.field);
});
}تشغيل الحزمة في CI
تؤتي الحزمة المؤتمتة ثمارها حين تعمل دون أن يشغّلها أحد. ففي خط أنابيب CI تكون اختبارات API مهمة واحدة: جلب الشيفرة، وتثبيت الاعتماديات، وتوجيه الحزمة إلى بيئة عبر المتغيرات والأسرار، وتشغيلها، ونشر النتائج، وإفشال خط الأنابيب إذا فشل اختبار. ويعرض مسار العمل أدناه هذه المهمة بصيغة GitHub Actions، ويعبّر GitLab CI وJenkins عن الخطوات نفسها في ملفاتهما. ومعظم الفرق تشغّل اختبارات API في عدة مواضع:
- مع كل طلب دمج: النواة السريعة من الحزمة، مقابل بيئة اختبار أو خدمة تُشغَّل داخل المهمة، فيرى صاحب التغيير الفشل قبل الدمج.
- بعد الدمج في الفرع الرئيسي: حزمة الانحدار كاملةً مقابل بيئة الاختبار المشتركة.
- بعد كل نشر: مجموعة قصيرة من اختبارات الدخان للقراءة فقط، مثل نقطة فحص الحالة (health) والمصادقة وقراءة واحدة لكل مورد أساسي، مقابل البيئة المنشورة.
- بشكل مجدول، ليلياً مثلاً: الاختبارات البطيئة والاختبارات مقابل البيئات التجريبية (Sandboxes) لدى الأطراف الثالثة، وهي لا ينبغي أن تعطّل كل دمج.
- كل تشغيل يرفع تقرير JUnit حتى عند فشل الاختبارات (
if: always())، وتبقى السجلات مع المهمة، فيمكن تشخيص الفشل دون إعادة تشغيله. - التشغيل الأحمر يوقف الدمج أو النشر؛ وفي GitHub يتطلب ذلك جعل المهمة فحص حالة إلزامياً (Required Status Check) في قاعدة حماية الفرع أو في مجموعة قواعد (Ruleset). والاختبار المتذبذب (Flaky) يُعيَّن له مسؤول، ثم يُصلَح أو يُعزَل، ولا يُعاد تشغيله حتى يخضرّ.
# .github/workflows/api-tests.yml
name: api-tests
on:
pull_request:
push:
branches: [main]
jobs:
api-tests:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npx playwright test
env:
API_BASE_URL: ${{ vars.API_BASE_URL }} # configuration variable
API_TOKEN: ${{ secrets.API_TOKEN }} # from the secret store
- uses: actions/upload-artifact@v7
if: always() # failures need evidence
with:
name: api-test-results
path: results/أخطاء شائعة
- أتمتة طلبات بدلاً من حالات اختبار: سكربت يرسل طلباً ولا يتحقق إلا من
200يضيف وقت تشغيل، لا تغطية. - اختبارات يعتمد بعضها على بعض، مثل اختبار حذف لا يعمل إلا بعد تشغيل اختبار الإنشاء. فهي تفشل حين تُشغَّل منفردة أو بترتيب آخر أو على التوازي.
- كتابة عناوين الأساس ورموز الوصول والمعرّفات داخل الاختبارات، فلا تعمل الحزمة إلا على جهاز واحد، وتنتهي الأسرار في المستودع.
- مشاركة بيانات تغيّرها الاختبارات: اختباران يعدّلان العميل نفسه، فتتوقف النتيجة على أيهما يعمل أولاً.
- التحقق من قيم دقيقة للحقول المولَّدة أو مقارنة الاستجابات كاملة، فتفشل الاختبارات بعد كل تغيير غير ضار، ويتعلم الفريق تجاهل التشغيلات الحمراء.
- إعادة تشغيل الاختبارات الفاشلة حتى تنجح. فإعادة المحاولة تخفي الاختبارات المتذبذبة والعيوب المتقطعة الحقيقية على حد سواء؛ حقّق في السبب، ثم أصلح أو اعزل مع تعيين مسؤول.
من منظور ضمان الجودة
في كثير من الفرق يملك المختبِرون ما تفحصه حزمة API، حتى حين يكتب المطورون جزءاً من شيفرتها. ففي جلسات التنقيح (Refinement) اتفق مع الفريق على أي فحوص القصة تُؤتمت على مستوى API وأيها يبقى يدوياً أو استكشافياً. وابنِ عميل API ومُنشئات البيانات مع المطورين، فهم يعرفون نقاط النهاية وبيانات البذر، وراجع شيفرة الاختبارات في طلبات الدمج كما تُراجَع شيفرة الإنتاج: هل لكل اختبار سلوك واحد واضح، ومرجع حقيقي للنتيجة المتوقعة، وبيانات خاصة به؟
تعامل مع كل تشغيل أحمر على أنه معلومة، وافرزه: عيب في المنتج، أو عيب في الاختبار، أو مشكلة في البيئة أو البيانات. فعيب المنتج يُكتب له تقرير خطأ فيه الطلب والاستجابة والتأكيد الفاشل مأخوذةً من تقرير CI؛ وعيب الاختبار يُصلَح في الحزمة؛ ومشكلة البيئة تُحال إلى مالكها. وراقب مدة تشغيل الحزمة وعدد مرات فشلها دون عيب في المنتج؛ فإذا زاد أيٌّ منهما تراجعت الثقة، وإصلاح ذلك جزء من العمل.
تمرين عملي
خطّط الحزمة المؤتمتة لواجهة قسائم الخصم
يضيف فريق قسائم الخصم إلى المتجر. والقواعد: percent عدد صحيح من 1 إلى 50؛ ويمكن تطبيق القسيمة حتى نهاية تاريخ validUntil؛ ولكل عميل أن يستخدم رمز القسيمة مرة واحدة. وتوجد بيئتان: test التي يُعاد ضبطها كل ليلة، وstaging المشتركة مع المختبِرين اليدويين. خطّط الاختبارات المؤتمتة لهذه الواجهة مستعيناً بمقتطف العقد أدناه.
تكفي الورقة والقلم. ويمكنك تجربة الطلبات في أداة REST API Tester، وصياغة أجسام الطلبات غير الصالحة بأداة API Test Data Generator.
POST /api/v1/coupons create a coupon (admin token)
{"code": "EID15", "percent": 15, "validUntil": "2027-03-31"}
201 coupon | 401 | 403 | 409 code exists | 422 errors[].field
GET /api/v1/coupons/{code} 200 coupon | 404
DELETE /api/v1/coupons/{code} 204 (admin token)
POST /api/v1/orders/{id}/coupon apply a coupon (customer token)
{"code": "EID15"}
200 order with discount | 404 unknown code | 422 expired or already used- اكتب قائمة الاختبارات، لكل منها سلوك واحد: الإيجابية، والسلبية المعتمدة على البيانات، وقواعد العمل.
- حدّد البيانات التي ينشئها كل اختبار، وكيف تبقى رموز القسائم فريدة، وكيف تُنظَّف البيانات.
- ارسم مخطط المجلدات ودوال العميل.
- اكتب قيم الإعدادات والأسرار التي تحتاجها الحزمة، ومتى يعمل كل جزء في CI.
إظهار الإجابة النموذجية
الإيجابية: إنشاء قسيمة ← 201، ثم يعيد GET القيم نفسها لـcode وpercent وvalidUntil. والحدود: percent بالقيمة 1 وبالقيمة 50 ← 201. وتطبيق قسيمة صالحة بخصم 15% على طلبية جديدة ← 200، والخصم يساوي 15% من مجموع الطلبية، يحسبه الاختبار من الاستجابة.
صفوف 422 المعتمدة على البيانات، يتحقق كل منها من الحقل المذكور في errors: percent بالقيم 0 و51 و15.5 و"15"؛ وغياب code؛ وvalidUntil بالقيمة 2027-02-30. وسلبيات أخرى: رمز القسيمة نفسه مرتين ← 409؛ ورمز غير معروف ← 404 في GET وفي التطبيق؛ والإنشاء بلا رمز وصول ← 401، وبرمز وصول لعميل ← 403.
قواعد العمل: تطبيق رمز القسيمة نفسه مرتين للعميل نفسه ← 422. وانتهاء الصلاحية حدّ زمني: القسيمة التي تاريخ validUntil الخاص بها هو اليوم ما زالت تُطبَّق ← 200، والقسيمة التي كان تاريخ validUntil الخاص بها أمس ← 422. فإذا رفضت نقطة الإنشاء التواريخ الماضية، فاطلب من المطورين سكربت بذر أو ساعة اختبار يمكن التحكم بها. واطرح هذا السؤال المفتوح مبكراً، ومعه سؤال المنطقة الزمنية التي تُحسب فيها «نهاية التاريخ».
البيانات: ينشئ كل اختبار قسيمته الخاصة برمز فريد، مثل AT- يليه معرّف التشغيل وعدّاد، ويحذفها بـDELETE في خطاف لاحق؛ وتُحذف الطلبيات التي أنشأتها اختبارات التطبيق بالطريقة نفسها، وتُزيل إعادةُ الضبط الليلية لبيئة test ما خلّفه أي تشغيل انقطع فجأة. وقاعدة «مرة واحدة لكل عميل» تُحتسب لكل عميل ورمز معاً، فمع الرموز الفريدة تستطيع الاختبارات كلها أن تتشارك عميل الاختبار الوحيد المُدرَج مسبقاً الذي يمثّله CUSTOMER_TOKEN، وتعمل مع ذلك على التوازي دون تصادم.
البنية: tests/coupons/ (الإنشاء والتحقق والتطبيق)، وapi/coupons-client.ts بالدوال create وget وapply وremove، وdata/coupon-builder.ts الذي يعيد قسيمة صالحة برمز فريد، وschemas/coupon.schema.json. وتقرأ الإعدادات API_BASE_URL وADMIN_TOKEN وCUSTOMER_TOKEN من متغيرات البيئة، وفي CI تأتي رموز الوصول من مخزن الأسرار.
CI: مع كل طلب دمج تعمل اختبارات القسائم (الإنشاء والتحقق والتطبيق) مقابل test، لأنها قليلة وسريعة؛ وبعد الدمج تعمل حزمة انحدار API كاملةً، ومنها اختبارات القسائم، مقابل test؛ وبعد النشر إلى staging يعمل فحص دخان للقراءة فقط، مثل GET على قسيمة مُدرَجة مسبقاً ببيانات البذر، لأن المختبِرين اليدويين يتشاركون staging. وكل تشغيل يرفع تقرير JUnit.
أسئلة شائعة
أي إطار أتعلّم أولاً لأتمتة اختبارات API؟
الإطار الذي يستخدمه فريقك، لأنك ستقرأ اختباراته وتوسّعها. وإن كانت لك حرية الاختيار فاتبع لغة مطوري المنتج: Playwright Test مع JavaScript أو TypeScript، وREST Assured مع Java، وpytest مع requests في Python. فالمشغّلات والتأكيدات والتجهيزات والتقارير تعمل بالطريقة نفسها فيها جميعاً، لذا يكون تعلّم الإطار الثاني أسرع بكثير.
هل تعمل اختبارات API المؤتمتة مقابل staging أم مقابل بيئة جديدة؟
مقابل الاثنتين، ولأغراض مختلفة. فالبيئة الجديدة لكل تشغيل أو لكل طلب دمج تعطي نتائج معزولة قابلة للتكرار، لأن أحداً غيرك لا يغيّر بياناتها. والبيئة المشتركة مثل staging تضيف الواقعية، كالإعدادات والاعتماديات الحقيقية، وفيها تعمل فحوص الدخان بعد النشر. وأبعِد الاختبارات التي تنشئ البيانات وتحذفها عن البيئات التي يعمل فيها الناس يدوياً، إلا إذا كانت بياناتها فريدة وتُنظَّف بعدها.
هل تحلّ اختبارات API المؤتمتة محل اختبار API اليدوي؟
لا. فهي تكرر الفحوص المعروفة بسرعة وموثوقية. أما استكشاف نقطة نهاية جديدة، ومساءلة عقد غامض، والحكم على ما إذا كانت رسالة الخطأ تفيد مطور العميل، واختبار أفكار لم يكتبها أحد بعد، فكلها تحتاج إلى إنسان. والأتمتة تمنحه وقتاً أطول لذلك.
كم اختبار API ينبغي أن يعمل مع كل طلب دمج؟
ما يكفي لتغطية السلوك الأساسي وحالات الخطأ الرئيسية لنقاط النهاية المتغيرة، مع بقاء المهمة سريعة بما يكفي لينتظر المطورون نتيجتها بارتياح. وانقل الاختبارات البطيئة واختبارات البيئات التجريبية لدى الأطراف الثالثة إلى تشغيلات ما بعد الدمج أو التشغيلات المجدولة.
المراجع
- Certified Tester Advanced Level Test Automation Engineering (CTAL-TAE) v2.0منهج ISTQB المتقدم لتصميم حلول أتمتة الاختبار وتنفيذها وصيانتها وبنيتها.
- The Practical Test Pyramid (Ham Vocke)شرح واسع الاستشهاد لكيفية الموازنة بين اختبارات الوحدة والتكامل والعقود والواجهة في محفظة الاختبارات الآلية.
- Playwright Documentationالتوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
- GitHub Actions documentationكيف تُعرَّف مسارات العمل والمهام والمشغّلات والمخرجات في GitHub Actions — حيث تعمل الاختبارات الآلية عند كل دفع أو طلب دمج.
تدرّب بأدوات متقن
اختبر نقاط نهاية REST بتحكم كامل في الطريقة والترويسات وجسم JSON، ثم أضف تأكيدات على الحالة والترويسات ومسارات JSON.
توليد كود جاهز للتنفيذ لطلبات REST API بلغات cURL وJavaScript fetch وaxios وPython requests وJava وC# وGo وPHP.
ولّد مصفوفة حالات اختبار منظّمة (إيجابية وسلبية ومصادقة وقيم حدّية) من مواصفة OpenAPI / Swagger.
ولّد أجسام طلبات ومجموعات بيانات لاختبار واجهات API من مخطط أو قائمة حقول، بما في ذلك المتغيرات غير الصالحة.
تحقّق من استجابات API وفق JSON Schema أو مخطط استجابة OpenAPI.
تنسيق YAML والتحقق من صحته بمسافات بادئة وعلامات اقتباس متسقة.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.