التحقق من صحة استجابات API
التحقق من صحة الاستجابة (Response Validation) هو الجزء من اختبار API الذي يقرّر ما إذا كانت الاستجابة صحيحة. فأنت تقارن الاستجابة الفعلية بالنتيجة المتوقعة طبقةً طبقة: رمز الحالة، والترويسات، والحقول الموجودة في الجسم، وأنواعها، وقيمها، وقواعد العمل التي تربط بينها، والزمن الذي استغرقته الاستجابة. وكل مقارنة تأكيد (Assertion): فحص دقيق واحد إما أن ينجح أو يفشل.
خذ مثلاً POST /api/v1/orders الذي ينشئ طلبية. عبارة «أعاد شيئاً ما» ليست اختباراً. أما «رمز الحالة 201، وContent-Type هو application/json، و$.status يساوي "pending"، و$.total يساوي مجموع الكمية مضروبةً في سعر الوحدة» فهي اختبار. والصيغة $.total من JSONPath، وهي لغة استعلام صغيرة لتحديد القيم داخل مستند JSON.
لماذا يهم
تُخفق الواجهات البرمجية بطرق لا تكشفها نظرة سريعة. فقد تحمل استجابة 200 OK رسالة خطأ في جسمها، وقد يتحول رقم بصمت إلى نص، وقد يُسقط المجموع أحد العناصر، وقد تتضمن استجابة الملف الشخصي تجزئة كلمة المرور. وكل عميل للواجهة، سواء كان تطبيق ويب أو تطبيق جوال أو تكاملاً لدى شريك، يرث هذه العيوب، وكثيراً ما لا يلاحظها حتى يتعطل شيء ما.
والتحقق يحدد أيضاً قيمة الاختبار نفسه. فالاختبار الذي يفحص رمز الحالة وحده ينجح كلما أجاب الخادم، فيمنح ثقة زائفة. أما التأكيدات الدقيقة فتجعل كل تشغيل دليلاً: التأكيد الفاشل يسمّي الحقل والقيمة المتوقعة والقيمة الفعلية، وهذا معظم ما يحتاجه تقرير خطأ جيد.
سبع طبقات لفحص الاستجابة
اقرأ الاستجابة من الخارج إلى الداخل: سطر الحالة يخبرك بما حدث، والترويسات تصف الرسالة، والجسم يحمل البيانات. وداخل الجسم افحص البنية أولاً (هل الحقول موجودة؟)، ثم الأنواع، ثم القيم، وأخيراً القواعد التي تربط عدة قيم. أما الزمن فلا يُكتب في الاستجابة، بل يقيسه العميل الذي تستخدمه، لكنه جزء من الفحص نفسه.
كل تأكيد يحتاج إلى مرجع اختبار (Test Oracle)، أي مصدر النتيجة المتوقعة: توثيق الواجهة، أو معايير القبول، أو قاعدة عمل، وليس الاستجابة نفسها أبداً. والمثال في هذا الدرس هو الاستجابة لطلب أنشأ طلبية للعميل 7 تضم عنصرين.
- رمز الحالة: الرمز الموثّق بالضبط. فالمورد المُنشأ يعيد
201، لا «أي رمز من فئة 2xx». تحقق من الرقم؛ فعبارة السبب («Created») للمعلومات فقط، ولا يرسلها HTTP/2 أصلاً. - الترويسات:
Content-Typeتذكر نوع وسائط الجسم، وهو هناapplication/json؛ وبعد201تحدّد ترويسةLocationالمورد الجديد. ويجعل RFC 9110 أسماء الترويسات غير حساسة لحالة الأحرف، فقارنها على هذا الأساس. - وجود الحقول: كل حقل مطلوب موجود، والحقول التي يجب ألا تُرسل أبداً، مثل
password، غائبة. - الأنواع:
idوcustomerIdعددان صحيحان، وtotalرقم، وitemsمصفوفة. ولو أُرسلت"7"بين علامتي تنصيص لكانت نصاً، وهذا عيب في النوع وإن بدت القيمة صحيحة. - القيم: ما أرسلته يعود دون تغيير (
customerIdيساوي 7)، وقيم التعداد (Enumerations) ضمن المجموعة المسموح بها (statusيساويpending)، والصيغ صحيحة (createdAtطابع زمني وفق RFC 3339 ينتهي بـZأو بإزاحة توقيت). - قواعد العمل: القيم متفقة فيما بينها. فـ
totalيساوي 2 × 99.95 + 1 × 50.00 = 249.90، والرقم في آخرLocationيساويid. - الزمن: تصل الاستجابة ضمن الميزانية التي اتفق عليها الفريق لنقطة النهاية (Endpoint) هذه. وهنا قاس العميل 184 ميلي ثانية مقابل ميزانية قدرها 1000 ميلي ثانية في بيئة الاختبار.
201 واحدة وسبع طبقات من الفحوص: رمز الحالة، والترويسات، والحقول المطلوبة والممنوعة، والأنواع، والقيم، وقاعدة عمل تربط total بالعناصر، والزمن الذي قاسه العميل.الاستجابة
HTTP/1.1 الجزء 1:201 Created الجزء 2:Content-Type: application/json Location: /api/v1/orders/9021 { الجزء 3:"id": 9021, الجزء 4:"customerId": 7, الجزء 5:"status": "pending", "items": [ { "sku": "A-100", "qty": 2, "unitPrice": 99.95 }, { "sku": "B-200", "qty": 1, "unitPrice": 50.00 } ], الجزء 6:"total": 249.90, "createdAt": "2026-10-01T09:30:12Z" }
يقيسه العميل، وليس جزءاً من الرسالة
response time: الجزء 7:184 ms
- 1رمز الحالةبالضبط
201: أُنشئت الطلبية. ولا يُتحقق من عبارة السبب.status == 201نجاح - 2الترويساتنوع الوسائط
application/json؛ وLocationينتهي بقيمةidللطلبية الجديدة.Location == /api/v1/orders/9021نجاح - 3وجود الحقولالحقول المطلوبة موجودة، والحقل الذي يجب ألا يُرسل أبداً غائب:
$.password → (no match)نجاح - 4الأنواعالعدد الصحيح 7 لا النص
"7"؛ وtotalرقم، وitemsمصفوفة.$.customerId type integerنجاح - 5القيمحالة الطلبية الجديدة
pending، وcustomerIdهو 7 الذي أرسلته.$.status == "pending"نجاح - 6قاعدة العمل
totalهو مجموعqty × unitPriceلكل العناصر.2 × 99.95 + 1 × 50.00 = 249.90نجاح - 7الزمنضمن الميزانية التي اتفق عليها الفريق لنقطة النهاية هذه: 1000 ميلي ثانية.
184 ms <= 1000 msنجاح
النسخة النصية
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/orders/9021
{
"id": 9021,
"customerId": 7,
"status": "pending",
"items": [
{ "sku": "A-100", "qty": 2, "unitPrice": 99.95 },
{ "sku": "B-200", "qty": 1, "unitPrice": 50.00 }
],
"total": 249.90,
"createdAt": "2026-10-01T09:30:12Z"
}الإشارة إلى الحقول بتعبيرات JSONPath
يحتاج كل تأكيد إلى عنوان للقيمة التي يفحصها. ولغة JSONPath، التي وحّدتها IETF في RFC 9535، تكتب هذا العنوان تعبيراً يبدأ من $، أي جذر المستند، وينزل مقطعاً بعد مقطع: .name لعضو في كائن، و[0] لموقع في مصفوفة، و[*] لكل العناصر، و.. لأي عمق، و[?(…)] للتصفية، حيث يمثّل @ العنصر الجاري اختباره. ويقبل RFC 9535 أيضاً التصفية دون الأقواس ([?@.qty>1])، لكن كثيراً من الأدوات التي سبقت المعيار تشترطها، ولذلك يكتبها هذا الدرس. وتختلف هذه الأدوات في تفاصيل أخرى أيضاً، فجرّب التعبير في أداتك أولاً.
النتيجة قائمة عُقد (nodelist): لا شيء، أو قيمة واحدة، أو قيم كثيرة. والنتيجة الفارغة ليست خطأ، بل تعني أن شيئاً لم يطابق. وهذا يسهّل التأكد من الغياب ويجعل الأخطاء الإملائية خطرة: $.totl لا يطابق شيئاً، لذا يجب أن يفشل أي تأكيد عليه، لا أن ينجح بصمت. والنتائج قيم بعد التحليل، فـ50.00 في النص هو الرقم 50.
- قيمة واحدة: يجب أن يساوي
$.statusالقيمة"pending". - الغياب: يجب ألا يعيد
$.passwordشيئاً. فالحقول الحساسة أو الداخلية لا تُكتشف إلا بتأكيد غيابها. - كل العناصر: اختر العناصر التي تخالف القاعدة وتوقّع نتيجة فارغة. فإذا لم يُعد
$.items[?(@.qty<1)]شيئاً، فلا يوجد عنصر كميته رقم أقل من 1. والمقارنة تتخطى العنصر الذي يغيب عنهqtyأو تكون قيمته نصاً، فاقرن القاعدة بفحصَي الوجود والنوع. وهذا يعمل في أي أداة، بما فيها الأدوات التي تقارن قاعدة القيمة بأول مطابقة فقط. - العدد: إذا توقعت عنصرين، فتحقق من طول
$.itemsبدلاً من فحص العنصر الأول وحده.
Expression Result on the example body
$.status "pending"
$.items[0].sku "A-100"
$.items[-1].unitPrice 50 last element
$.items[*].qty 2, 1 every element
$..sku "A-100", "B-200" any depth
$.items[?(@.qty>1)].sku "A-100" filter
$.password (no match) absent, as requiredمثال تطبيقي: قائمة الفحص لطلبية جديدة
يقول التوثيق: «POST /api/v1/orders ينشئ طلبية ويعيد 201 Created، وترويسة Location تحمل عنوان الطلبية، والطلبيةَ نفسها بصيغة JSON. حالة الطلبية الجديدة pending. وtotal هو مجموع qty × unitPrice لكل العناصر».
أرسل الطلب customerId بقيمة 7 وعنصرين: A-100 بكمية 2 وB-200 بكمية 1. وكتابة تأكيد واحد في كل سطر، مع طبقته ومرجعه، تعطيك قائمة فحص يمكنك تنفيذها يدوياً، أو لصقها في أداة تحقق، أو أتمتتها لاحقاً.
- القيم التي تتغير في كل تشغيل، مثل
idوcreatedAt، تُفحص بالنوع والصيغة والعلاقة (رقمLocationيساويid، وcreatedAtقريب من وقت إرسال الطلب)، ولا تُفحص بقيمة محددة أبداً. - القيم التي تتحكم فيها تُفحص بدقة: أرسلت العميل 7 وعنصرين، فتتوقع عودتهما كما هما.
- يُحسب المجموع المتوقع من عناصر الاستجابة وفق القاعدة الموثّقة، فالقاعدة هي المرجع لا الاستجابة. وقارن الأرقام بوصفها أرقاماً:
249.90و249.9نصان مختلفان لكنهما رقم JSON واحد. - وهناك فحص يتجاوز هذه الاستجابة: طلب
GETعلى عنوانLocationيجب أن يعيد الطلبية نفسها، وهذا يثبت أنها حُفظت ولم تُردَّد فحسب.
# Layer Assertion Oracle
1 status status == 201 documentation
2 headers Content-Type contains application/json documentation
3 headers Location == /api/v1/orders/{id} documentation
4 fields $.id, $.status, $.items, $.total exist documentation
5 fields $.password does not exist security rule
6 types $.id integer, $.total number, $.items array documentation
7 values $.customerId == 7, $.items length == 2 the request
8 values $.status == "pending" business rule
9 values $.createdAt is RFC 3339 with Z or an offset documentation
10 rule $.total == sum of qty * unitPrice (249.90) business rule
11 timing response time <= 1000 ms (measured: 184 ms) team budgetما الذي تتحقق منه في كل استجابة، وما الذي تتركه
لا تحتاج كل نقطة نهاية إلى أحد عشر فحصاً، لكن كل استجابة تستحق حداً أدنى، بما فيها استجابات الأخطاء: فاستجابة الخطأ تأخذ رمز 4xx الموثّق بالضبط، ونوع محتوى JSON، وجسم خطأ بالشكل الموثّق.
- دائماً: رمز الحالة بالضبط، ونوع الوسائط في
Content-Type، وجسم يُحلَّل بوصفه JSON، والحقول المطلوبة، وغياب الحقول الممنوعة. أما استجابة204 No Contentفلا جسم لها، فتتحقق فيها من رمز الحالة والترويسات فقط. - البيانات التي أرسلتها أو يمكنك توقعها: قيم محددة. والبيانات المولَّدة مثل المعرّفات والطوابع الزمنية والرموز المميزة (Tokens): النوع والصيغة والنطاق والعلاقات.
- قواعد العمل: تأكيد واحد على الأقل لكل قاعدة تنفذها نقطة النهاية.
- الزمن: فحص للميزانية في كل استدعاء. والعينة البطيئة الواحدة سبب لإعادة القياس، وليست بعدُ عيباً في الأداء.
- ما لا دلالة له فلا تتحقق منه: ترتيب الأعضاء في كائن JSON (يعرّف RFC 8259 الكائن بأنه مجموعة غير مرتبة)، وترتيب الترويسات ذات الأسماء المختلفة، وحالة أحرف أسماء الترويسات، والمسافات البيضاء في الجسم.
- ترتيب المصفوفة: لا تتحقق منه إلا إذا وثّقت الواجهة ترتيباً للفرز، وعندها تحقق منه صراحةً.
أخطاء شائعة
- التحقق من رمز الحالة وحده؛ فاستجابة
200 OKتحمل{"error": "out of stock"}في جسمها تنجح في مثل هذا الاختبار. - نسخ الاستجابة الحالية لتكون النتيجة المتوقعة. فإن كانت الاستجابة خاطئة أصلاً، صار الاختبار يحمي العيب؛ خذ القيم المتوقعة من المرجع.
- التحقق من قيم محددة لبيانات مولَّدة مثل المعرّفات والطوابع الزمنية، فيفشل الاختبار في التشغيل التالي مع أن شيئاً لم يخطئ.
- عدّ نتيجة JSONPath الفارغة نجاحاً. فخطأ إملائي مثل
$.totlلا يطابق شيئاً، ويجب أن يفشل التأكيد عليه. - مقارنة الأرقام بوصفها نصاً، فيفشل
249.9أمام249.90، أو المقارنة المتساهلة، فينجح النص"249.90"حيث كان المطلوب رقماً. - فحص عنصر واحد من المصفوفة وافتراض أن البقية مثله: العنصر الأول صحيح، والثالث مجموعه نص.
من منظور ضمان الجودة
في الفريق يبدأ التحقق من الاستجابات قبل أن توجد الواجهة. فعند مراجعة توثيق الواجهة أو وصف OpenAPI، اسأل عن كل استجابة: ما رمز الحالة بالضبط؟ أي الحقول مطلوب وأيها يقبل null؟ ما القيم والصيغ المسموح بها؟ وأي قاعدة تربط بينها؟ والإجابة الناقصة عيب في التوثيق، وإصلاحه في هذه المرحلة أرخص ما يكون.
وعند التنفيذ، احفظ قائمة الفحص مع حالة الاختبار ليكررها مختبِر آخر، وسجّل القيمة الفعلية لكل تأكيد فاشل. فتقرير خطأ API الجيد يذكر الطلب، والقيمة المتوقعة والفعلية للفحص الفاشل، والمرجع: «$.items[2].total هو النص "75.50"، والتوثيق يعرّف total بأنه رقم». واتفق مع المطورين على الفحوص التي ستُؤتمت، ليذهب الجهد اليدوي إلى القواعد والأسئلة التي لا يطرحها سكربت.
تمرين عملي
تحقّق من قائمة طلبيات مصفّاة
يقول التوثيق: «GET /api/v1/orders?status=paid يعيد 200 وapplication/json. يحتوي الجسم على page وpageSize وitems؛ وتضم items عدداً من الطلبيات لا يتجاوز pageSize، حالة كل منها paid، مرتبةً حسب createdAt من الأحدث إلى الأقدم. وtotal رقم. الزمن المستهدف: 1000 ميلي ثانية». وقد تلقّى العميل الاستجابة أدناه خلال 2340 ميلي ثانية.
اكتب قائمة الفحص لهذه الاستجابة، ثم حدّد الفحوص الناجحة والفاشلة. استخدم الورقة والقلم، أو الصق الاستجابة في «مدقّق استجابات API» و«مختبِر JSON Path».
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
{
"page": 1,
"pageSize": 20,
"items": [
{ "id": 9021, "status": "paid", "total": 249.90,
"createdAt": "2026-10-01T09:30:12Z" },
{ "id": 9017, "status": "pending", "total": 120.00,
"createdAt": "2026-09-30T18:05:44Z" },
{ "id": 9010, "status": "paid", "total": "75.50",
"createdAt": "2026-09-29T11:20:03Z" }
]
}- اكتب الفحوص طبقةً طبقة: رمز الحالة، والترويسات، والحقول، والأنواع، والقيم، وقواعد العمل، والزمن.
- اكتب كل فحص للجسم تعبيرَ JSONPath مع نتيجته المتوقعة؛ وفي قواعد «كل عنصر» اختر العناصر التي تخالف القاعدة.
- نفّذ الفحوص وسجّل النجاح أو الفشل مع القيمة الفعلية.
- اكتب لكل فشل عنواناً لتقرير خطأ في سطر واحد.
إظهار الإجابة النموذجية
ناجح: رمز الحالة 200؛ و$.page يساوي 1؛ و$.items مصفوفة من 3 عناصر، أي لا تتجاوز pageSize (20)؛ وقيم createdAt هي 1 أكتوبر ثم 30 سبتمبر ثم 29 سبتمبر، فالترتيب من الأحدث صحيح؛ وكل طابع زمني ينتهي بـZ.
فشل في الترويسات: Content-Type هي text/html; charset=utf-8 بينما الجسم JSON. عنوان التقرير: «GET /api/v1/orders يعيد JSON موسوماً بأنه text/html».
فشل في قاعدة العمل: $.items[?(@.status!='paid')].id يعيد 9017، والمطلوب نتيجة فارغة. عنوان التقرير: «مرشّح status=paid يعيد الطلبية 9017 وحالتها pending».
فشل في الأنواع: $.items[2].total هو النص "75.50" لا رقم. وفحص نوع كل عنصر يدوياً بطيء؛ أما التحقق بالمخطط (Schema Validation) فيفحص كل العناصر دفعة واحدة. عنوان التقرير: «الطلبية 9010 تعيد total نصاً».
تحذير في الزمن: 2340 ميلي ثانية تتجاوز الهدف البالغ 1000 ميلي ثانية. والعينة الواحدة لا تكفي؛ كرّر الطلب بضع مرات، وأبلغ عنه إذا بقي الزمن فوق الهدف.
أسئلة شائعة
ما الفرق بين JSONPath وJSON Pointer؟
يحدد JSON Pointer (RFC 6901) قيمة واحدة بالضبط عبر مسارها، مثل /items/0/sku. أما JSONPath (RFC 9535) فاستعلام: يمكن لأحرف البدل والتصفية والنزول إلى أي عمق أن تختار لا شيء، أو قيمة واحدة، أو قيماً كثيرة، مثل $.items[*].sku. ولذلك تستخدم أدوات التحقق من الاستجابات، مثل أدوات هذا الدرس، لغة JSONPath، لأن التأكيدات كثيراً ما تتعلق بعدة قيم أو بغيابها.
أليس فحص رمز الحالة كافياً في اختبار الدخان؟
لفحص سريع من نوع «هل الخدمة تعمل؟» قد يكفي رمز الحالة ونوع المحتوى. أما الاختبار الذي يدّعي أن الميزة تعمل فلا يكفيه ذلك: رمز الحالة لا يقول شيئاً عن الجسم، واستجابة 200 قد تحمل مجموعاً خاطئاً أو رسالة خطأ. أضف على الأقل الحقول المطلوبة وقاعدة عمل واحدة.
هل أتحقق من الاستجابات يدوياً أم بالأتمتة؟
بالاثنين، في أوقات مختلفة. التصميم والتشغيل الأول يدويان: تقرأ التوثيق، وتختار مرجع كل فحص، وتنظر إلى الاستجابة الحقيقية. وعندما تستقر قائمة الفحص تصبح التأكيدات نفسها فحوصاً مؤتمتة تعمل مع كل بناء، ويذهب وقتك اليدوي إلى نقاط النهاية الجديدة والأسئلة الاستكشافية.
ما الفرق بين هذا والتحقق بالمخطط؟
المخطط يصف البنية المشتركة بين كل الاستجابات الصحيحة: الحقول المطلوبة والأنواع والصيغ والقيم المسموح بها. لكنه لا يُجري حسابات، فلا يستطيع التحقق من أن total يساوي مجموع العناصر، ولا يعرف طلبك، فلا يستطيع التحقق من أن التصفية التي طلبتها طُبّقت، أو أن القيمة هي التي أرسلتها. استخدم المخطط للشكل، والتأكيدات للمعنى.
المراجع
- RFC 9110: HTTP Semanticsمعاني أساليب HTTP وحقول الترويسات ورموز الحالة والتفاوض على المحتوى، بغضّ النظر عن إصدار البروتوكول.
- RFC 9535: JSONPath: Query Expressions for JSONصياغة JSONPath القياسية — الجذر والأبناء والأحفاد والمحدِّدات الشاملة ومرشّحات الاختيار — وكيفية تقييم الاستعلام.
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Formatقواعد JSON: الكائنات والمصفوفات والنصوص والأرقام والقيم الثلاث الثابتة، وUTF-8 كترميز للتبادل.
- ISTQB Glossaryالتعريفات القياسية لمصطلحات الاختبار — حالة الاختبار واختبار الانحدار والخطورة والأولوية وتحليل القيم الحدّية وغيرها.
تدرّب بأدوات متقن
تحقّق من استجابة API وفق التوقعات: الحالة والترويسات المطلوبة وJSON Schema وقواعد الحقول.
تقييم تعبيرات JSONPath ($.store.book[*].author) على JSON وعرض النتائج المطابقة فوراً.
اختبر نقاط نهاية REST بتحكم كامل في الطريقة والترويسات وجسم JSON، ثم أضف تأكيدات على الحالة والترويسات ومسارات JSON.
استكشاف JSON كشجرة قابلة للطي مع المسارات والأنواع وعدد القيم.
حلّل استجابة API: البنية وأنواع الحقول وقيم null والتكرارات والأحجام ومشكلات جودة البيانات المحتملة.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.