اختبار قواعد البيانات لمهندسي الجودة

يتحقق اختبار قواعد البيانات (Database testing) من أن البيانات التي يخزّنها التطبيق صحيحة وكاملة ومتسقة ومحمية — لا من أن الشاشة تعرض «تم الحفظ» فحسب. يخبرك اختبار الواجهة أو API بما يعلنه التطبيق؛ أما فحص قاعدة البيانات فيخبرك بما حدث فعلاً: أي الصفوف كُتبت، وأي القيود صمدت، وهل ما زالت السجلات المرتبطة متوافقة، وهل تُرك شيء خلفه.

هذه الصفحة مدخل الموضوع. ترسم خريطة المجالات التي يغطيها المختبِر — التحقق من عمليات CRUD، والقيود والسلامة المرجعية، والمنطق المخزّن، والاتساق بين الأنظمة، وبيانات الاختبار، والأداء والأمن — وتقود إلى صفحتين أعمق: كتابة استعلامات SQL للتحقق، واختبار المعاملات والتزامن والترحيلات.

ماذا يغطي اختبار قواعد البيانات

معظم العيوب التي تصل إلى قاعدة البيانات لا تُرى في الواجهة. قد تُعرض صفحة تأكيد الطلب على نحو مثالي بينما يحمل صف الطلب إجمالياً خاطئاً، أو ترتبط دفعة بطلب آخر، أو تبقى أصناف طلب ملغى محجوزة. يلتقي المستخدمون بهذه العيوب لاحقاً — في كشف حساب شهري، أو استرداد يفشل، أو تقرير لا تتطابق أرقامه — حين يصبح تتبّعها مكلفاً.

اختبار قواعد البيانات ليس مستوى اختبار مستقلاً، بل مجموعة فحوص تُطبَّق في مستويات التكامل والنظام والقبول حيثما تُنشأ البيانات أو تتغير. ومرجعه (Test oracle) هو نموذج البيانات نفسه: المخطط (Schema) وقيوده، وقواعد العمل التي تربط جدولاً بآخر، والأرقام التي يجب أن تتطابق.

  • الاختبار البنيوي (Schema testing) — الجداول والأعمدة والأنواع والأطوال والقيم الافتراضية والقيود والفهارس مطابقة للتصميم ولسكربتات الترحيل.
  • اختبار البيانات الوظيفي — كل إنشاء وقراءة وتحديث وحذف تنفّذه الواجهة أو API يترك بالضبط الصفوف التي ينبغي أن يتركها.
  • اختبار السلامة (Integrity) — قواعد المفاتيح والتفرّد وNOT NULL وCHECK والمفتاح الأجنبي ترفض البيانات الخاطئة، وتبقى السجلات المرتبطة متسقة.
  • اختبار المنطق — الإجراءات المخزّنة والقوادح (Triggers) والعروض والمهام المجدولة تعطي النتائج الصحيحة للمدخلات العادية والحدّية والخاطئة.
  • اختبار المعاملات والتزامن — سلوك «الكل أو لا شيء»، ونتائج صحيحة حين يغيّر مستخدمان البيانات نفسها في اللحظة ذاتها (تتناوله صفحة المعاملات والترحيلات بعمق).
  • اختبار الترحيل (Migration) — تغييرات المخطط والبيانات تُنفَّذ بشكل صحيح على أحجام واقعية وتُبقي التطبيق العامل يعمل.
  • الاختبار غير الوظيفي — أداء الاستعلامات على الجداول الكبيرة، وضبط الصلاحيات، وحماية البيانات الشخصية.

التحقق من CRUD: نفّذ عبر المنتج وتأكّد في قاعدة البيانات

التقنية الأساسية بسيطة: نفّذ الإجراء كما يفعل المستخدم أو العميل — عبر الواجهة أو API — ثم استعلم قاعدة البيانات لتتأكد من الأثر. إدخال الصفوف مباشرة ثم تسمية ذلك اختباراً للميزة يتجاوز شيفرة التطبيق التي يُفترض أنك تختبرها. الإدخال المباشر لتجهيز الشروط المسبقة، لا لتشغيل السلوك.

افحص في كل عملية الصف المستهدف وما يجاوره. الإنشاء ينبغي أن يكتب صفاً واحداً بكل حقل مربوط بشكل صحيح، مع القيم التي يولّدها الخادم (المعرّفات والطوابع الزمنية والحالة) وإنشاء الصفوف الفرعية. التحديث ينبغي أن يغيّر الأعمدة المقصودة فقط ويحدّث updated_at. الحذف ينبغي أن يزيل أو يحذف منطقياً (Soft delete) ما ينص عليه المتطلب بالضبط ويعامل الصفوف الفرعية كما صُمّم. والقراءة ينبغي أن تعيد المخزَّن كما هو — بما في ذلك النص العربي والرموز التعبيرية والأطوال القصوى والكسور العشرية — دون اقتطاع أو إعادة تقريب.

على سبيل المثال، بعد إنشاء طلب عبر API يضم وحدتين من صنف سعره 75.00 ريالاً ووحدة من صنف سعره 40.00 ريالاً، يتحقق الاستعلام في نهاية هذا القسم من صف الطلب وصفوف أصنافه والقيم التي يضبطها الخادم. ومن عيوب CRUD النموذجية التي يكشفها هذا النوع من الفحص:

  • ربط الحقول — تقبل API الحقل delivery_notes لكن العمود يبقى NULL.
  • الدقة — مبلغ 19.995 يُخزَّن 19.99 في جدول و20.00 في آخر.
  • الترميز — أسماء عربية تُحفظ «????» لأن عموداً أو الاتصال يستخدم مجموعة محارف غير Unicode.
  • المناطق الزمنية — تخزّن خدمة created_at بالتوقيت المحلي وتخزّنه أخرى بتوقيت UTC.
  • الكتابة الجزئية — صف الطلب موجود وأصنافه غير موجودة بعد خطأ تحقق في الصنف الثالث.
-- After POST /api/orders returned order_id 1042 for customer 317
SELECT order_id, customer_id, status, total_amount, created_at, updated_at
FROM orders
WHERE order_id = 1042;

SELECT product_sku, quantity, unit_price
FROM order_items
WHERE order_id = 1042
ORDER BY order_item_id;

-- Expected: customer_id = 317, status = 'PENDING',
-- total_amount = 2 x 75.00 + 1 x 40.00 = 190.00,
-- exactly two item rows, created_at set by the server (UTC)
نفّذ عبر الواجهة أو API لتكتب شيفرة التطبيق الصفوف، ثم تأكّد باستعلام قاعدة البيانات على المفتاح الذي أعاده؛ والإدخال المباشر لتجهيز الشروط المسبقة فقط.
  • يختبر الميزة

    • الاختبار
    • المنتج الواجهة أو API وشيفرة التطبيق
    • قاعدة البيانات
    1. 1 من الاختبار إلى قاعدة البيانات: جهّز الشروط المسبقة فقط الإدخال المباشر مقبول هنا، مثل وجود العميل 317
    2. 2 من الاختبار إلى المنتج: أنشئ الطلب كما يفعل العميل العميل 317: وحدتان من صنف سعره 75.00 ريالاً ووحدة من صنف سعره 40.00 ريالاً POST /api/orders
    3. 3 من المنتج إلى قاعدة البيانات: شيفرة التطبيق تكتب الصفوف صف الطلب وصفوف أصنافه؛ ويضبط الخادم المعرّف والحالة والطوابع الزمنية
    4. 4 من المنتج إلى الاختبار: يعيد order_id 1042 ردّ
    5. 5 من الاختبار إلى قاعدة البيانات: استعلم على المفتاح المُعاد orders ثم order_items WHERE order_id = 1042
    6. 6 من قاعدة البيانات إلى الاختبار: صفوف تُقارن بالقيم المتوقعة ردّ العميل 317، الحالة PENDING، المجموع 190.00، صفّان للأصناف بالضبط، ويضبط الخادم created_at بتوقيت UTC

    النتيجة: التنفيذ يمرّ عبر المنتج؛ ولا يذهب مباشرة إلى قاعدة البيانات إلا تجهيز الشروط المسبقة والتحقق.

  • ليس اختباراً للميزة

    • الاختبار
    • المنتج الواجهة أو API وشيفرة التطبيق
    • قاعدة البيانات
    1. 1 من الاختبار إلى قاعدة البيانات: أدخِل صفوف الطلب والأصناف مباشرة بدلاً من إنشاء الطلب عبر API
    2. 2 من الاختبار إلى قاعدة البيانات: استرجعها بـ SELECT
    3. 3 من قاعدة البيانات إلى الاختبار: الصفوف التي كتبتها ردّ

    النتيجة: المنتج لا يعمل أصلاً: هذا يتجاوز شيفرة التطبيق التي يُفترض أنك تختبرها. تنبيه

جرّبها: منسّق SQL جرّبها: مدقّق استعلامات SQL

القيود: القواعد التي تفرضها قاعدة البيانات

القيود (Constraints) خط الدفاع الأخير لقاعدة البيانات: حتى لو كان في تحقق التطبيق خلل، يرفض المخطط الجيد البيانات التي تكسر النموذج. لاختبارها وجهان. أولاً، تأكد أن القيد موجود — اقرأ المخطط أو ملف الترحيل، لا وثيقة التصميم وحدها. ثانياً، تأكد أن التطبيق يتعامل مع الرفض بلباقة: رسالة تحقق واضحة أو استجابة API من فئة 4xx، لا 500 تحمل خطأ قاعدة البيانات الخام.

المحركات تختلف، فافحص المحرك الذي تشحن عليه. لا يفرض MySQL قيود CHECK إلا بدءاً من الإصدار 8.0.16 (كانت الإصدارات الأقدم تحللها وتتجاهلها)، ولا يفرض SQLite المفاتيح الأجنبية إلا إذا فعّلها الاتصال بـ PRAGMA foreign_keys = ON. القيد الموجود في ملف الترحيل وغير المفروض وقت التشغيل ملاحظة تستحق التسجيل.

  • المفتاح الأساسي (Primary key) — كل صف معرَّف بشكل فريد. يجب ألا ينشئ الطلب المُعاد صفاً ثانياً بمعرّف جديد لحدث العمل نفسه؛ فتلك مشكلة عدم تكرار الأثر (Idempotency) لا يحلها المفتاح وحده.
  • UNIQUE — سجّل البريد نفسه مرتين، بما في ذلك باختلاف حالة الأحرف وبمسافات في البداية أو النهاية. اتفق مع الفريق هل Layla.H@example.com وlayla.h@example.com عميل واحد، ثم تأكد أن قاعدة البيانات توافق.
  • NOT NULL — أرسل الحقل غائباً وفارغاً ومسافات فقط. النص الفارغ ليس NULL في PostgreSQL ولا MySQL، لذا لا يمنع NOT NULL وحده الأسماء الفارغة.
  • CHECK — الكمية أكبر من صفر، والمبالغ غير سالبة، والحالة ضمن مجموعة مسموح بها. اختبر الحدود (0 و1 والحد الأقصى) وخطوة واحدة خارجها.
  • المفتاح الأجنبي (Foreign key) — أنشئ طلباً لمعرّف عميل غير موجود. يجب أن يفشل الإدخال وأن تعيد API خطأً ذا معنى.
  • القيم الافتراضية والأنواع والأطوال — الحقول المحذوفة تأخذ القيمة الافتراضية الموثقة، وكل عمود يتسع لأطول مدخل صالح. عمود بطول 50 حرفاً خلف حقل تسمح الواجهة فيه بـ 100 حرف خلل اقتطاع ينتظر الظهور.

جرّبها: مولّد مجموعات الأرقام الحدّية جرّبها: مولّد مجموعات الأحرف الخاصة جرّبها: مولّد مجموعات بيانات Unicode

العلاقات والسلامة المرجعية والحذف المتتالي

السلامة المرجعية (Referential integrity) تعني أن كل مرجع يشير إلى شيء موجود: كل صنف طلب ينتمي إلى طلب موجود، وكل دفعة إلى طلب موجود، وكل طلب إلى عميل موجود. عند التصريح بالمفاتيح الأجنبية تفرض قاعدة البيانات ذلك. وحين لا يُصرَّح بها — وهو شائع بين الخدمات المصغّرة، وفي المخططات المجزأة أو القديمة، وفي الجداول التي تحمّلها مهام دفعية — يجب أن يفرضه التطبيق، وتصبح الصفوف اليتيمة (Orphan rows) من أكثر عيوب البيانات التي يجدها المختبِر.

قواعد التتالي (Cascade) تحدد ما يحدث للصفوف الفرعية حين يُحذف الأب أو يتغير مفتاحه. ON DELETE CASCADE يحذف الأبناء مع الأب؛ وRESTRICT أو NO ACTION يرفض الحذف ما دام للأب أبناء؛ وSET NULL يُبقي الابن ويفرغ المرجع. كل منها صحيح لعلاقة وخطِر لأخرى: تتالي حذف العميل إلى الفواتير قد يدمّر سجلات يلزم العمل الاحتفاظ بها. اختبر كل علاقة مقابل المتطلب، لا مقابل ما يفعله المخطط مصادفة.

  • احذف أباً عبر المنتج، ثم استعلم كل جدول فرعي عن صفوف ما زالت تشير إليه.
  • افحص الحذف المنطقي: العميل الموسوم بالحذف ينبغي أن يختفي من القوائم، بينما قد يلزم أن تبقى طلباته السابقة قابلة للقراءة.
  • شغّل استعلامات كشف الأيتام بعد الاستيراد الدفعي وترحيل البيانات والمهام الفاشلة — تعرض صفحة SQL نمط LEFT JOIN … IS NULL.

الإجراءات المخزّنة والقوادح وقواعد الخلفية

حين يعيش منطق العمل في قاعدة البيانات — إجراء مخزّن (Stored procedure) يغلق شهراً، أو قادح يكتب صف تدقيق، أو عرض يغذّي تقريراً — عامله كأي شيفرة أخرى: حدّد المدخلات والمخرجات المتوقعة، واختر الحالات بتقسيم التكافؤ وتحليل القيم الحدّية. استدعِ الإجراءات مباشرة في قاعدة اختبار ببيانات مجهّزة، وشغّلها كذلك عبر مسار التطبيق الذي يستخدمها.

القوادح تستحق عناية خاصة لأنها تعمل بصمت. تأكد أنها تنطلق في كل مسار ينبغي أن يطلقها (الإدراج والتحديث والحذف؛ عبارات الصف الواحد والعبارات الجماعية)، وأنها لا تنطلق مرتين، وأن الفشل داخل القادح يتراجع عن العبارة التي سببته بدل أن يترك نصف العمل منجزاً.

  • قادح التدقيق — تغيير حالة الطلب يكتب صف تدقيق واحداً بالضبط بالقيمة القديمة والجديدة والمستخدم المنفّذ وطابع زمني من الخادم.
  • حدود الإجراء — شغّل إجراء نهاية الشهر لشهر فبراير في سنة كبيسة، ولشهر لا طلبات فيه إطلاقاً.
  • تحقق الخلفية — القاعدة المفروضة في الواجهة مفروضة أيضاً في API، وفي قاعدة البيانات حيث صُمّم ذلك. تجاوز الواجهة باستدعاء API مباشر لتتأكد.

اتساق البيانات بين الخدمات والذاكرة المؤقتة وفهارس البحث

تحتفظ الأنظمة الحديثة بنسخ من البيانات: نسخة قراءة متماثلة، وذاكرة مؤقتة (Cache)، وفهرس بحث، ومستودع تقارير، وقاعدة بيانات خدمة مصغّرة أخرى. كل نسخة قد تنحرف عن مصدر الحقيقة. سعر حُدّث في قاعدة البيانات وما زال مخزّناً مؤقتاً، أو طلب ظاهر في خدمة الطلبات ومفقود من الفوترة، أو عميل محذوف ما زال يظهر في البحث — هذه عيوب اتساق، وتجدها بمقارنة النسخ.

المطابقة (Reconciliation) هي التقنية: عُدّ واجمع المجموعة نفسها في مكانين وقارن. الأعداد حسب الحالة واليوم، ومجاميع المبالغ، وقوائم المعرّفات الموجودة في جهة دون الأخرى تكشف معظم الانحراف. وحيث يكون النسخ غير متزامن، اتفق مع الفريق على التأخير المقبول — فالاتساق النهائي (Eventual consistency) قرار تصميم لا عيب — واختبر أن النسخ تتقارب خلاله، بما في ذلك بعد الأعطال وإعادة المحاولة وإعادة التشغيل.

  • طلبات اليوم: العدد والإجمالي في قاعدة الطلبات، وفي خدمة الفوترة، وفي التقرير المالي.
  • فهرس البحث: كل معرّف منتج نشط في قاعدة البيانات موجود في الفهرس، ولا يبقى فيه أي معرّف محذوف.
  • إبطال الذاكرة المؤقتة: حدّث سجلاً ثم اقرأه عبر API فوراً وبعد انتهاء عمر الذاكرة المؤقتة؛ إن نص التصميم على أن الكتابة تبطلها، فالقراءتان تعيدان القيمة الجديدة.
  • التدفقات القائمة على الأحداث: أوقف المستهلك، وأنشئ سجلات، وأعد تشغيله، وتأكد أن شيئاً لم يُفقد ولم يُطبَّق مرتين.

جرّبها: مولّد عبارات SQL IN جرّبها: مولّد قوائم SQL VALUES

بيانات الاختبار: اصطناعية ومزروعة وقابلة لإعادة الضبط

تحتاج اختبارات قواعد البيانات إلى بيانات معروفة — عملاء معروفين وطلبات معروفة وإجماليات معروفة — كي تُكتب النتيجة المتوقعة قبل تشغيل الاختبار. ابنِها عن قصد: سكربت زرع (Seed) ينشئ خط الأساس، وكل اختبار يضيف ما يحتاجه ثم ينظّف أو يعمل داخل معاملة يُتراجع عنها، ويمكن إعادة القاعدة كلها إلى خط الأساس في دقائق.

لا تنسخ البيانات الشخصية من الإنتاج إلى بيئات الاختبار. يحكم نظام حماية البيانات الشخصية السعودي (PDPL)، الذي تشرف عليه سدايا (SDAIA)، معالجة البيانات الشخصية، وبيئة اختبار تحوي أسماء عملاء حقيقيين وأرقام هوياتهم وهواتفهم وعناوينهم هي معالجة لبيانات شخصية بضوابط أضعف من الإنتاج. استخدم البيانات الاصطناعية افتراضياً. وإن احتجت فعلاً إلى بيانات بشكل الإنتاج، كما في تمرين ترحيل مثلاً، فاتفق على الطريقة مع مسؤول حماية البيانات: البيانات المقنّعة قد تبقى بيانات شخصية إذا أمكن ربطها بشخص.

  • ازرع من SQL مولَّد: سكربتات CREATE TABLE وINSERT لخط أساس واقعي، محفوظة في نظام إدارة الإصدارات بجانب الترحيلات.
  • ضمّن القيم الصعبة عمداً: الأطوال القصوى، والمبالغ الصفرية، والنص العربي والديفاناغاري، والرموز التعبيرية، والفواصل العليا في الأسماء، وتواريخ نهايات الأشهر والسنوات.
  • اختر استراتيجية إعادة الضبط: التراجع بعد كل اختبار، أو التفريغ وإعادة الزرع لكل مجموعة، أو استعادة لقطة أو قاعدة قالب، أو تشغيل قاعدة جديدة في حاوية لكل تشغيل لخط الأنابيب.
  • لاختبارات الأداء والترحيل، ولّد البيانات بأعداد صفوف قريبة من الإنتاج، لا عشرة صفوف.

جرّبها: مولّد مجموعات بيانات SQL جرّبها: مولّد جمل SQL INSERT جرّبها: محوّل CSV إلى SQL جرّبها: محوّل JSON إلى SQL

الأداء على مستوى المختبِر: الفهارس وخطط الاستعلام وN+1

لا تحتاج إلى ضبط قاعدة البيانات لتجد عيوب أدائها. السبب المعتاد بسيط: استعلام كان سريعاً على 200 صف اختبار يمسح ملايين الصفوف في الإنتاج لأن العمود الذي يرشّح عليه بلا فهرس. اختبر بحجم واقعي، وراقب أزمنة الاستجابة مع نمو البيانات، واطلب خطة الاستعلام حين يبطؤ شيء.

يعرض EXPLAIN كيف تنوي قاعدة البيانات تنفيذ الاستعلام — هل تستخدم فهرساً أم تمسح الجدول كله، وبأي ترتيب تربط الجداول. يوفّر PostgreSQL وMySQL كلاً من EXPLAIN وEXPLAIN ANALYZE، ويوفّر SQLite الأمر EXPLAIN QUERY PLAN. ينفّذ EXPLAIN ANALYZE العبارة فعلاً، فلا تشغّله على UPDATE أو DELETE خارج معاملة ستتراجع عنها. ولا يلزمك قراءة كل سطر في الخطة: مسح كامل لجدول كبير في استعلام يُفترض أن يعيد بضعة صفوف يستحق تقرير خطأ.

  • استعلامات N+1 — صفحة قائمة تحمّل 50 طلباً باستعلام واحد، ثم تنفّذ استعلاماً إضافياً لكل طلب لجلب عميله: 51 رحلة بدل واحدة أو اثنتين. تظهر في سجل الاستعلامات أو تتبّع APM دفعةً من عبارات شبه متطابقة.
  • الترقيم العميق — الترقيم المعتمد على OFFSET يبطؤ في الصفحات المتأخرة من الجداول الكبيرة؛ اختبر الصفحة 1 والصفحة 5,000.
  • الاستعلامات غير المحدودة — تصديرات وتقارير بلا حدود تحمّل جدولاً كاملاً في الذاكرة.
  • فهارس مفقودة على المفاتيح الأجنبية وعلى الأعمدة المستخدمة في WHERE وJOIN وORDER BY للاستعلامات المتكررة.

الأمن: الحقن والصلاحيات والبيانات الشخصية

يحدث حقن SQL (SQL injection) حين تُلصق مدخلات المستخدم في نص SQL. ودفاع OWASP هو الاستعلامات المُعامَلة (Parameterised queries / Prepared statements) في كل مكان، مع تحقق بقائمة مسموح بها للأجزاء التي لا يمكن أن تكون معاملات، مثل اسم عمود الترتيب. كمختبِر، تحقق من التوقع بدل كتابة هجمات: أدخل في كل حقل قيماً تحوي علامات اقتباس وفواصل منقوطة وعلامات تعليق ومحارف SQL خاصة أخرى، وتوقّع أن تُخزَّن وتُعرض حرفياً دون خطأ قاعدة بيانات أو تتبّع مكدس أو تغيّر في السلوك. ظهور رسالة خطأ من قاعدة البيانات في أي استجابة ملاحظة بحد ذاته. أما اختبار الحقن المعمّق فمكانه تقييم أمني مصرّح به.

مبدأ أقل الصلاحيات (Least privilege) يحدّ من الضرر حين يقع خطأ. ينبغي أن يملك حساب التطبيق ما يحتاجه فقط — عادةً القراءة والكتابة على جداوله، بلا تغيير للمخطط وبلا صلاحيات إدارية — وأن تعمل الترحيلات بحساب منفصل. وينبغي أن يستخدم المختبِرون وأدوات التقارير حسابات للقراءة فقط.

  • اعرف الحساب الذي يتصل به التطبيق وما يُسمح له به؛ تطبيق إنتاجي يعمل بحساب مالك قاعدة البيانات ملاحظة.
  • ابحث عن البيانات الشخصية حيث لا ينبغي أن تكون: سجلات التطبيق، ورسائل الخطأ، وأحداث التحليلات، وتصديرات CSV، والنسخ الاحتياطية على الأقراص المشتركة، وبيئات الاختبار.
  • تأكد أن كلمات المرور مخزّنة بخوارزمية مخصّصة لتجزئة كلمات المرور (مثل Argon2id أو bcrypt أو scrypt أو PBKDF2، وهي تملّح كل كلمة مرور) لا بتجزئة عامة سريعة، وأن الرموز والأسرار الأخرى مجزّأة أو مشفّرة كما صُمّم، ولا تُخزَّن نصاً صريحاً أبداً.
  • أعمدة التدقيق — created_at وupdated_at وcreated_by وupdated_by — يضبطها الخادم ولا تُقبل من العميل؛ يتغير updated_at وupdated_by كلما تغيّر السجل، بينما لا يتغير created_at وcreated_by أبداً بعد الإدراج.

جرّبها: مولّد مجموعات الأحرف الخاصة

العيوب النموذجية والأتمتة وCI

تُؤتمت فحوص قاعدة البيانات جيداً لأن نتائجها المتوقعة دقيقة. ضع استعلامات السلامة والمطابقة في مجموعة اختبارات API نفسها: استدعِ API، واستعلم القاعدة، وتحقق. شغّل المجموعة في خط الأنابيب على قاعدة مرحَّلة ومزروعة حديثاً، كي يثبت كل بناء أن الترحيلات تُطبَّق بنظافة وأن قواعد البيانات ما زالت صامدة. واحتفظ بمجموعة صغيرة من استعلامات السلامة للقراءة فقط — الأيتام والتكرارات والإجماليات غير المتطابقة — لتشغيلها على بيئة Staging بعد كل نشر وعلى جدول زمني.

حين يفشل فحص، فالصفوف التي يعيدها هي الدليل: أرفق الاستعلام ونتيجته بتقرير الخطأ. وتبدو العيوب المكتشفة بهذه الطريقة عادةً هكذا:

  • «إجمالي الطلب يختلف عن مجموع أسطر الأصناف عند تطبيق خصم على الصنف الأخير»
  • «حذف العميل يترك صفوف دفعات يتيمة (لا مفتاح أجنبي على payments.order_id)»
  • «إنشاء عميل مكرر عند النقر المزدوج على زر التسجيل»
  • «أسماء المنتجات العربية مقتطعة في order_items.product_name»
  • «البحث ما زال يعيد منتجات حُذفت قبل أكثر من 24 ساعة»
  • «صفحة سجل الطلبات تنفّذ استعلاماً لكل طلب (N+1): 4 ثوانٍ مع 200 طلب»
  • «API تعيد 500 مع خطأ قاعدة البيانات الخام عندما يكون البريد موجوداً مسبقاً»

الأسئلة الشائعة

هل يحتاج المختبِرون إلى معرفة SQL لاختبار قواعد البيانات؟

نعم، على مستوى القراءة: SELECT مع WHERE، والربط الداخلي واليساري، وGROUP BY مع HAVING، والتجميعات مثل COUNT وSUM، وEXISTS، والمعاملات الأساسية. هذا يكفي للتحقق من كل تغيير يجريه التطبيق تقريباً. أما تصميم المخطط وضبط الأداء فيبقيان عمل المطورين ومديري قواعد البيانات.

هل أختبر عبر الواجهة أم مباشرة في قاعدة البيانات؟

كلاهما، ولغرضين مختلفين. شغّل السلوك عبر الواجهة أو API لتعمل شيفرة التطبيق، ثم استعلم القاعدة لتتأكد من الأثر. استخدم SQL المباشر للشروط المسبقة والتحقق وفحوص السلامة — لا بديلاً عن الميزة.

هل يمكن استخدام نسخة من بيانات الإنتاج للاختبار؟

ليس وفيها بيانات شخصية. وفق نظام حماية البيانات الشخصية السعودي، تبقى بيانات العملاء الحقيقية في بيئة الاختبار بيانات شخصية تجري معالجتها. استخدم البيانات الاصطناعية افتراضياً، وأشرك مسؤول حماية البيانات إن كانت البيانات بشكل الإنتاج ضرورية فعلاً.

ما السلامة المرجعية؟

القاعدة التي تقضي بأن يشير كل مرجع بين الجداول إلى صف موجود — لا أصناف طلب بلا طلب، ولا دفعات بلا طلب. تفرضها المفاتيح الأجنبية في قاعدة البيانات؛ ومن دونها تظهر صفوف يتيمة بعد الحذف والاستيراد والمهام الفاشلة.

ما الفرق بين سلامة البيانات واتساق البيانات؟

سلامة البيانات (Data integrity) أن تكون البيانات صحيحة وكاملة وملتزمة بقواعدها: أنواع الحقول والقيود والسلامة المرجعية، وتفرضها قاعدة البيانات نفسها بالمفاتيح والقيود. أما اتساق البيانات (Data consistency) فأن تتطابق النسخ المختلفة من البيانات نفسها: نسخة القراءة المتماثلة والذاكرة المؤقتة وفهرس البحث وقواعد بيانات الخدمات الأخرى. تُختبر السلامة بمحاولة كسر القيود، ويُختبر الاتساق بالمطابقة (Reconciliation): عدّ المجموعة نفسها في مكانين ومقارنتها، والبحث عن المعرّفات الموجودة في جهة دون الأخرى.

ما الأدوات التي يحتاجها اختبار قواعد البيانات؟

عميل SQL متصل بحساب للقراءة فقط، وواجهة المنتج وAPI الخاصة به، وطريقة لتوليد بيانات الاختبار وإعادة ضبطها، وإطار اختبار يستطيع تنفيذ SQL بعد استدعاءات API. تساعد مولّدات SQL والبيانات في أدوات متقن في سكربتات الزرع وقيم الحالات الحدّية.

المراجع

  • Constraints — PostgreSQL DocumentationPostgreSQL Global Development Groupقيود التحقق وعدم القيمة الفارغة والتفرّد والمفتاح الأساسي والمفتاح الأجنبي والإقصاء — القواعد التي تفرضها قاعدة البيانات ويتحقق منها المختبِر.
  • Transactions — PostgreSQL DocumentationPostgreSQL Global Development Groupما المعاملة: عدة خطوات تُطبَّق كلها أو لا شيء عبر BEGIN وCOMMIT وROLLBACK، ولا يراها الآخرون قبل التثبيت.
  • Transaction Isolation Levels — MySQL 8.4 Reference ManualOracle (MySQL)مستويات العزل الأربعة في SQL كما ينفّذها InnoDB، ومستوى REPEATABLE READ هو الافتراضي فيه.
  • OWASP Cheat Sheet Series: SQL Injection PreventionOWASP Foundationالاستعلامات ذات المعاملات، والإجراءات المخزنة المستخدمة بأمان، والتحقق بقائمة السماح، وأقل الصلاحيات — الدفاعات التي ينبغي أن يؤكدها اختبار قاعدة البيانات.
  • SQL InjectionOWASP Foundationكيف يعمل حقن SQL ولماذا الاستعلامات المُعامَلة، لا بناء النصوص، هي الدفاع.
  • Personal Data Protection Law (English translation)الهيئة السعودية للبيانات والذكاء الاصطناعي (سدايا) · 2023النظام السعودي لحماية البيانات الشخصية — ولماذا ينبغي أن تستخدم بيئات الاختبار بيانات اصطناعية بدلاً من نسخ سجلات العملاء الحقيقية.
  • ISO/IEC 9075-1:2023 — Information technology — Database languages SQL — Part 1: Framework (SQL/Framework)ISO/IEC · 2023الجزء الإطاري من معيار SQL الدولي، الذي تنفّذه لهجات PostgreSQL وMySQL وSQLite وغيرها بدرجات متفاوتة.
  • ISTQB Certified Tester Foundation Level (CTFL) v4.0ISTQB · 2023المنهج التأسيسي لاختبار البرمجيات: مستويات الاختبار وأنواعه وتقنيات تصميم الاختبارات وإدارة الاختبار والأدوات.

تمرين عملي

تحقّق في قاعدة البيانات من إلغاء طلب

المطلوب

تختبر خدمة توصيل بقالة خيالية في بيئة Staging. يتيح البناء 3.8.0 للعميل إلغاء طلبه ما دام الطلب لم يغادر المتجر. ألغيت طلباً اختبارياً مدفوعاً بالبطاقة عبر تطبيق Android كما يفعل العميل، ثم تحققت من النتيجة في قاعدة البيانات بحساب للقراءة فقط. تجد في المادة أدناه المتطلبات الستة، ومقتطفاً من المخطط (Schema)، وما جرى في التطبيق وعلى الخادم، ومخرجات استعلاماتك الخمسة.

احكم على النتيجة كما يعلّم الدليل: اكتب الحالة المتوقعة أولاً، ثم قارنها بالصفوف الفعلية، وانظر أبعد من الصف المستهدف: إلى ما يجاوره، وإلى نسخة البيانات التي يعرضها التطبيق، وإلى المخطط نفسه. ولكل اختلاف اذكر مجال اختبار قواعد البيانات الذي ينتمي إليه. تكفي الورقة والقلم؛ وخصّص لذلك ⁦40–50⁩ دقيقة. ويعرض سجل التطبيق السبب العربي في جسم الطلب بصيغة تهريب JSON (\u)، وتعيده أداة «مفكّك ترميز Unicode» نصاً مقروءاً؛ وإن كتبت SQL في الخطوة 4 فيمكنك فحص صياغته بأداة «مدقّق استعلامات SQL» من قائمة أدوات الدليل.

المادة
خدمة توصيل بقالة (مثال خيالي): متجر ويب وتطبيقا Android وiOS.
بيئة Staging، البناء 3.8.0، وقاعدة البيانات PostgreSQL. حسابات اختبار اصطناعية فقط.
قاعدة التصميم: يُخزَّن كل طابع زمني بتوقيت UTC، وتوقيت الرياض هو UTC+3.
الجديد في الإصدار 3.8: يستطيع العميل إلغاء طلبه ما دام الطلب لم يغادر المتجر.

المتطلبات: إلغاء الطلب
المتطلب CN-1: لا يُلغى إلا طلب في الحالة PENDING أو PACKING. وفي أي حالة أخرى
  تعيد API الاستجابة 409 ORDER_NOT_CANCELLABLE ولا يتغير شيء.
المتطلب CN-2: يبقى صف الطلب، مع status بقيمة CANCELLED، وcancelled_at = وقت الإلغاء،
  وcancel_reason = نص العميل كما كتبه، ويتقدّم updated_at.
  وتبقى صفوف order_items كي يظل سجل الطلبات يعرضها.
المتطلب CN-3: يُحذف كل حجز مخزون للطلب، كي يمكن بيع المخزون مجدداً.
المتطلب CN-4: إن دُفع الطلب بالبطاقة يُنشأ صف استرداد واحد: amount = إجمالي الطلب،
  وstatus بقيمة PENDING، مرتبطاً بدفعة البطاقة.
المتطلب CN-5: كل تغيير في الحالة يكتب صفاً واحداً بالضبط في order_status_audit.
المتطلب CN-6: تقرأ «طلباتي» في التطبيقات عبر ذاكرة مؤقتة (Cache) عمر مُدخلاتها 5 دقائق،
  ويزيل تغيير الحالة مُدخل الطلب، فتكون القراءة التالية محدَّثة.

مقتطف من المخطط (كل الجداول في قاعدة البيانات نفسها)
CREATE TABLE stock_reservations (
  reservation_id BIGINT PRIMARY KEY,
  order_id BIGINT NOT NULL,
  product_sku VARCHAR(40) NOT NULL,
  qty INTEGER NOT NULL CHECK (qty > 0)
);
CREATE TABLE refunds (
  refund_id BIGINT PRIMARY KEY,
  order_id BIGINT NOT NULL REFERENCES orders (order_id),
  payment_id BIGINT NOT NULL REFERENCES payments (payment_id),
  amount NUMERIC(10,2) NOT NULL CHECK (amount > 0),
  status VARCHAR(20) NOT NULL,
  created_at TIMESTAMP NOT NULL
);
orders: order_id PRIMARY KEY, customer_id, status, total_amount, cancel_reason,
        created_at, updated_at, cancelled_at
order_items.order_id REFERENCES orders (order_id) ON DELETE CASCADE
payments: payment_id PRIMARY KEY, order_id, amount, status
order_status_audit: audit_id, order_id, old_status, new_status, changed_by, changed_at
يملأ order_status_audit قادحٌ (Trigger) معرَّف بـ AFTER UPDATE OF status ON orders.
لا فهرس على stock_reservations غير مفتاحه الأساسي.

الاختبار عبر تطبيق Android كما يفعل العميل
العميل 5021 (حساب اختبار)، والطلب 88412 أُنشئ يوم 5 أكتوبر 2026 الساعة 07:12:40 UTC،
وحالته PACKING، ودُفع بالبطاقة: الدفعة 77031 بمبلغ 55.00 ريال.
وللطلب حجزا مخزون، حجز لكل صنف:
  حليب طازج 1 لتر (MLK-1L): الكمية 2، وسعر الوحدة 6.50 ريال
  أرز بسمتي 5 كغ (RCE-5KG): الكمية 1، وسعر الوحدة 42.00 ريال
الساعة 07:15:04 UTC: «إلغاء الطلب»، والسبب مكتوب بالعربية: طلبت بالخطأ

سجل الشبكة في التطبيق (يتخلى التطبيق عن طلب API بعد 10 ثوانٍ ويعيد المحاولة مرة واحدة)
07:15:04 POST /api/orders/88412/cancel
         {"reason": "\u0637\u0644\u0628\u062a \u0628\u0627\u0644\u062e\u0637\u0623"}
         لا استجابة خلال 10 ثوانٍ
07:15:15 POST /api/orders/88412/cancel
         إعادة محاولة تلقائية بالجسم (Body) نفسه
         200 {"orderId": 88412, "status": "CANCELLED"}
سجل الخادم
07:15:04 POST /api/orders/88412/cancel 200 (10.6 s)
07:15:15 POST /api/orders/88412/cancel 200 (0.5 s)
قائمة «طلباتي» في التطبيق: الساعة 07:15:16 تعرض الطلب 88412 «قيد التجهيز»،
  والساعة 07:20:30 تعرضه «ملغى».

فحوص قاعدة البيانات بعد ذلك (بحساب للقراءة فقط)
Q1 SELECT status, cancel_reason, created_at, updated_at, cancelled_at
   FROM orders WHERE order_id = 88412;
   status: CANCELLED
   cancel_reason: ???? ??????
   created_at: 2026-10-05 07:12:40
   updated_at: 2026-10-05 07:12:40
   cancelled_at: 2026-10-05 10:15:15
Q2 SELECT product_sku, quantity, unit_price FROM order_items
   WHERE order_id = 88412 ORDER BY order_item_id;
   MLK-1L | 2 | 6.50
   RCE-5KG | 1 | 42.00
Q3 SELECT reservation_id, product_sku, qty FROM stock_reservations
   WHERE order_id = 88412;
   5530182 | RCE-5KG | 1
Q4 SELECT refund_id, payment_id, amount, status, created_at FROM refunds
   WHERE order_id = 88412 ORDER BY refund_id;
   9101 | 77031 | 55.00 | PENDING | 2026-10-05 07:15:14
   9102 | 77031 | 55.00 | PENDING | 2026-10-05 07:15:15
Q5 SELECT audit_id, old_status, new_status, changed_by, changed_at
   FROM order_status_audit WHERE order_id = 88412 ORDER BY audit_id;
   70411 | PENDING | PACKING | system | 2026-10-05 07:13:05
   70415 | PACKING | CANCELLED | customer:5021 | 2026-10-05 07:15:14
   70416 | CANCELLED | CANCELLED | customer:5021 | 2026-10-05 07:15:15
الخطوات
  1. قبل أن تقرأ Q1 إلى Q5، اكتب الحالة المتوقعة بعد إلغاء صحيح واحد للطلب 88412: صف orders، وorder_items، وstock_reservations، وrefunds، وorder_status_audit، والاستجابة لطلب API المُعاد، وما ينبغي أن تعرضه «طلباتي».
  2. قارن مخرجات الاستعلامات والسجلين وقائمة التطبيق بالحالة المتوقعة. ولكل اختلاف اكتب الدليل (الجدول والصف والقيم)، والمتطلب الذي يخالفه، ومجال اختبار قواعد البيانات الذي ينتمي إليه.
  3. اذكر ما هو صحيح ولا ينبغي الإبلاغ عنه، مع السبب.
  4. راجع مقتطف المخطط: أي القواعد لا يحميها أي قيد، وماذا تقترح، وأي استعلامات سلامة للقراءة فقط تضيفها إلى الفحوص التي تعمل بعد كل نشر؟
  5. رتّب نتائجك، واكتب عنوان أول تقرير خطأ ونتيجته المتوقعة وأدلته.
إظهار الإجابة النموذجية
الإجابة النموذجية

الخطوة 1: الحالة المتوقعة. في orders يبقى صف الطلب 88412 موجوداً، وقيمة status فيه CANCELLED، وcancel_reason هو طلبت بالخطأ حرفياً، وcancelled_at نحو الساعة 07:15 بتوقيت UTC يوم 5 أكتوبر 2026، وupdated_at تقدّم إلى اللحظة نفسها، وcreated_at باقٍ على 07:12:40. وفي order_items يبقى الصفان دون تغيير (MLK-1L ⁦2 × 6.50⁩ وRCE-5KG ⁦1 × 42.00⁩). وفي stock_reservations لا صف للطلب 88412 (CN-3). وفي refunds صف واحد بالضبط للدفعة 77031، قيمة amount فيه 55.00 (⁦2 × 6.50 + 42.00⁩) وstatus فيه PENDING (CN-4). وفي order_status_audit الصف السابق ⁦PENDING → PACKING⁩ وصف جديد واحد بالضبط ⁦PACKING → CANCELLED⁩ (CN-5). أما طلب API المُعاد فيجد الطلب 88412 ملغى (CANCELLED) بالفعل، وهذه الحالة ليست PENDING ولا PACKING، فيتلقى وفق CN-1 الاستجابة 409 ORDER_NOT_CANCELLABLE ولا يغيّر شيئاً. وتعرض «طلباتي» الحالة «ملغى» في أول قراءة بعد الإلغاء (CN-6).

الخطوة 2: الاختلافات. F1، وهو الأخطر: نُفّذ الإلغاء مرتين. فقد نجح طلب API الأول فعلاً، لكن الخادم احتاج إلى 10.6 ثانية، فتخلّى التطبيق عنه بعد 10 ثوانٍ وأعاد المحاولة؛ ووجدت إعادة المحاولة الطلب 88412 ملغى بالفعل، فكان يجب أن تتلقى 409، لكنها تلقت 200 وأعادت العمل كله. يُظهر Q4 استردادين، 9101 و9102، بمبلغ 55.00 لكل منهما للدفعة الواحدة 77031: أي إن 110.00 ريال تنتظر الاسترداد لطلب قيمته 55.00 ريال. وصف التدقيق 70416 (⁦CANCELLED → CANCELLED⁩) وقيمة cancelled_at المطابقة لطلب API الثاني أثران للتشغيل الثاني نفسه. وهذا يخالف CN-1 وCN-4. المجال: اختبار البيانات الوظيفي (الإنشاء يجب أن يكتب صفاً واحداً بالضبط)، ومشكلة عدم تكرار الأثر (Idempotency) التي يذكرها الدليل تحت المفتاح الأساسي: يجب ألا ينشئ طلب API المُعاد صفاً ثانياً لحدث العمل نفسه. وطريقة الإصلاح قرار الفريق: فحص الحالة الذي يشترطه CN-1 أصلاً، ويُفضَّل معه مفتاح عدم تكرار الأثر (Idempotency key) لأن التطبيق يعيد المحاولة من تلقاء نفسه، فتتلقى إعادة المحاولة نتيجة الطلب الأول بدلاً من خطأ. أما طلبان متطابقان في اللحظة نفسها فيحتاجان إلى فحص يصمد أمام التزامن أيضاً، وهو ما تتناوله صفحة المعاملات والترحيلات. وفي كل الأحوال يجب أن تنتهي قاعدة البيانات باسترداد واحد.

F2: لم يُحرَّر أحد حجوزات المخزون. ما زال Q3 يعيد الحجز 5530182 للصنف RCE-5KG، بينما اختفى حجز MLK-1L، أي إن CN-3 نُفّذ جزئياً. المجال: CRUD، فالحذف يجب أن يزيل بالضبط ما ينص عليه المتطلب، وهذه كتابة جزئية. يبقى كيس الأرز محجوزاً فلا يمكن بيعه، وقد تحجب طلبات ملغاة أخرى المخزون بالطريقة نفسها. أعد الاختبار بطلبات من صنف واحد وصنفين وخمسة أصناف لتعرض النمط على المطوّر.

F3: ضاع سبب الإلغاء المكتوب بالعربية. يعرض Q1 القيمة ???? ?????? حيث كتب العميل طلبت بالخطأ: أربع علامات استفهام ثم ست، لكلمتين من أربعة أحرف وستة أحرف، وهو عيب الترميز الوارد في قسم CRUD من الدليل. وهذا يخالف CN-2. تأكد أولاً أن علامات الاستفهام مخزّنة فعلاً، لا أن عميل SQL لديك يعرض النص هكذا فحسب: يعيد الاستعلام SELECT octet_length(cancel_reason) FROM orders WHERE order_id = 88412 العدد 11 إن كان المخزَّن عشر علامات استفهام ومسافة، بينما يشغل السبب العربي 21 بايتاً بترميز UTF-8. وعلامات الاستفهام المخزّنة لا يمكن إعادتها إلى كلمات العميل. وجسم الطلب يُفكّ إلى طلبت بالخطأ بالضبط (\u0637 هو الحرف الأول، ط)، أي إن النص غادر التطبيق سليماً وتلف في جهة الخادم في طريقه إلى قاعدة البيانات. اسأل كيف تحوّل الخدمة النص، وأي ترميز يستخدمه الاتصال وقاعدة البيانات (في PostgreSQL يُضبط الترميز لقاعدة البيانات كلها لا لكل عمود)، واحفظ السبب نفسه عبر متجر الويب وتطبيق iOS لترى هل المشكلة في مسار واحد أم في كل المسارات، وأضف إلى إعادة الاختبار نصاً بالديفاناغارية ورمزاً تعبيرياً (Emoji).

F4: قيمة cancelled_at بتوقيت الرياض. فهي 10:15:15، بينما يضع سجل الخادم وصف التدقيق 70416 والاسترداد 9102 اللحظة نفسها عند 07:15:15 بتوقيت UTC. والتقدّم بثلاث ساعات بالضبط يعني أن القيمة خُزّنت بالتوقيت المحلي خلافاً لقاعدة التصميم. المجال: CRUD (المناطق الزمنية). وكل ما يُحسب منها خاطئ: يبدو الطلب كأنه أُلغي بعد ثلاث ساعات ودقيقتين و35 ثانية من إنشائه بدلاً من دقيقتين و35 ثانية، ويضعه تقرير الإلغاءات بحسب الساعة في الساعة الخطأ.

F5: قيمة updated_at لا تتقدّم. فهي ما زالت تساوي created_at (07:12:40)، مع أن الطلب تغيّر مرتين: إلى PACKING الساعة 07:13:05 (صف التدقيق 70411)، ثم إلى CANCELLED. وقاعدة الدليل أن كل تحديث يقدّم updated_at وأن الخادم هو من يضبطه. ولأن التغيير السابق إلى PACKING فاته ذلك أيضاً، فالعيب على الأرجح في طريقة صيانة updated_at عموماً، لا في شيفرة الإلغاء وحدها. واسأل هل توجد مهمة تقرأ التغييرات بحسب updated_at، كتصدير تزايدي إلى التقارير: فمثل هذه المهمة لن تلتقط هذا الإلغاء أبداً.

F6: عرضت «طلباتي» حالة قديمة. ففي الساعة 07:15:16، بعد أن أجاب الخادم بـ CANCELLED، ظل التطبيق يعرض الطلب 88412 «قيد التجهيز»، ولم يعرضه «ملغى» إلا الساعة 07:20:30، وقد انتهى عندها عمر مُدخل الذاكرة المؤقتة البالغ 5 دقائق. ويشترط CN-6 أن يزيل التغيير المُدخل، فيجب أن تكون أول قراءة بعد الإلغاء محدَّثة. المجال: اتساق البيانات بين النسخ (إبطال الذاكرة المؤقتة). والعميل الذي يرى «قيد التجهيز» قد يحاول الإلغاء مجدداً أو يتصل بالدعم. وللتأكد، قارن استجابة API بما يعرضه التطبيق فور الإلغاء، واقرأ كل 30 ثانية حتى يتطابقا؛ واسأل هل تقرأ القائمة أيضاً من نسخة قراءة متماثلة، فتلك نسخة أخرى قد تتأخر.

F7، وهو أصغر، في المنطق المخزّن: صف التدقيق 70416 يسجّل ⁦CANCELLED → CANCELLED⁩، وهذا ليس تغييراً في الحالة. فالقادح (Trigger) المعرَّف بـ AFTER UPDATE OF status ينطلق كلما ضبطت عبارة UPDATE ذلك العمود، ولو على القيمة التي يحملها أصلاً؛ وفي PostgreSQL يقصره شرط مثل WHEN (OLD.status IS DISTINCT FROM NEW.status) على التغييرات الحقيقية. وإصلاح F1 يخفي هذا الصف لكنه لا يغيّر القادح، فبلّغ عنه منفصلاً. وسجّل ملاحظة أخرى دون أن تسميها عيباً بعد: استغرق طلب API الأول 10.6 ثانية، أي أكثر من مهلة التطبيق، وهذا ما سبّب إعادة المحاولة. اطلب تتبّعاً (Trace) للطلب وخطط الاستعلامات في عملية الإلغاء. والفهرس المفقود على stock_reservations.order_id أحد الاحتمالات، لكن سرعة طلب API الثاني (0.5 ثانية) توحي بأن الوقت ربما ذهب إلى مكان آخر، كاستدعاء خدمة أخرى مثلاً.

الخطوة 3: ما هو صحيح ولا يُبلَّغ عنه. بقي صف الطلب ولم يُحذف (CN-2). وما زال صفا order_items موجودين، وهذا ما يشترطه CN-2 من أجل سجل الطلبات؛ فإن توقعت اختفاءهما فقد اختبرت مقابل افتراض لا مقابل المتطلب، ولا أهمية هنا لـ ON DELETE CASCADE على order_items لأن شيئاً لم يُحذف. ولكل استرداد المبلغ الصحيح، 55.00 ريال (⁦2 × 6.50 + 42.00⁩)، والدفعة الصحيحة؛ والعيب أنهما اثنان. وصف التدقيق 70411 هو التغيير السابق المشروع إلى PACKING. وقيمة changed_by هي customer:5021 وقد ضبطها الخادم، فجسم الطلب لم يحمل إلا السبب.

الخطوة 4: ما لا يحميه المخطط. (أ) لا شيء يمنع استرداداً ثانياً للدفعة نفسها. فإن كانت القاعدة استرداداً واحداً لكل دفعة، لرفض القيد UNIQUE (payment_id) على refunds الاسترداد 9102؛ وإن كان الاسترداد الجزئي مخططاً له، فالحماية يجب أن تكون عدم تكرار الأثر في API مع فحص يضمن ألا تتجاوز الاستردادات مبلغ الدفعة. اسأل أي القاعدتين تنطبق، واختبر أن الإدراج المرفوض يصل إلى التطبيق بالاستجابة 409 لا بـ 500 تحمل خطأ قاعدة البيانات الخام. (ب) يقبل refunds.status أي نص: اقترح قيد CHECK بالقيم المسموح بها واطلب قائمتها. (ج) لا مفتاح أجنبياً على stock_reservations.order_id مع أن الجدولين في قاعدة البيانات نفسها، فقد يشير حجز إلى طلب غير موجود؛ اقترح المفتاح الأجنبي، وإلى أن يُضاف افحص الصفوف اليتيمة (Orphan rows). (د) ولا فهرس على هذا العمود أيضاً، فإيجاد حجوزات طلب ما يقرأ الجدول كله؛ اطلب خطة الاستعلام على حجم قريب من الإنتاج. (هـ) اطلب قراءة تعريف القادح الذي يقف وراء F7.

استعلامات سلامة للقراءة فقط للفحوص التي تعمل بعد كل نشر، ويجب ألا يعيد أيٌّ منها صفوفاً. الاستردادات المكررة، متى أكّد الفريق قاعدة استرداد واحد لكل دفعة: SELECT payment_id, COUNT(*) AS refund_count, SUM(amount) AS refunded FROM refunds GROUP BY payment_id HAVING COUNT(*) > 1. الاستردادات التي تتجاوز الدفعة: SELECT p.payment_id, p.amount, SUM(r.amount) AS refunded FROM payments p JOIN refunds r ON r.payment_id = p.payment_id GROUP BY p.payment_id, p.amount HAVING SUM(r.amount) > p.amount. الحجوزات الباقية على طلبات ملغاة: SELECT r.reservation_id, r.order_id FROM stock_reservations r JOIN orders o ON o.order_id = r.order_id WHERE o.status = 'CANCELLED'. الحجوزات اليتيمة: SELECT r.reservation_id, r.order_id FROM stock_reservations r LEFT JOIN orders o ON o.order_id = r.order_id WHERE o.order_id IS NULL. صفوف التدقيق التي لا تسجّل تغييراً: SELECT audit_id, order_id FROM order_status_audit WHERE old_status = new_status. الطلبات الملغاة التي لم يتقدّم فيها updated_at: SELECT order_id FROM orders WHERE status = 'CANCELLED' AND (cancelled_at IS NULL OR updated_at < cancelled_at). وشغّل الاستعلامات نفسها في خط الأنابيب بعد اختبارات API، على قاعدة بيانات مرحَّلة ومزروعة حديثاً.

الخطوة 5: الترتيب وأول تقرير. يأتي F1 أولاً: فهو يدفع المال مرتين، ويحدث كلما جعلت استجابة بطيئة التطبيقَ يعيد المحاولة. ثم F2 (مخزون تحجبه طلبات ملغاة)، وF3 (كلمات العميل تضيع نهائياً)، وF6 (يرى العميل حالة خاطئة مدة تصل إلى خمس دقائق)، وF4 وF5 (طوابع زمنية خاطئة وقديمة تضلّل التقارير وأي مهمة مزامنة)، ثم F7. عنوان أول تقرير: «إعادة محاولة الإلغاء تنشئ استرداداً ثانياً: للطلب 88412 استردادان بقيمة 55.00 ريال لدفعة بطاقة واحدة». الخطوات: ألغِ عبر API طلباً مدفوعاً بالبطاقة في الحالة PACKING، ثم أرسل طلب الإلغاء نفسه مرة أخرى؛ أو ألغِ من التطبيق بينما تستغرق الاستجابة الأولى أكثر من 10 ثوانٍ. النتيجة المتوقعة: يتلقى طلب API الثاني 409 ORDER_NOT_CANCELLABLE (أو نتيجة الطلب الأول إن أضاف الفريق مفتاح عدم تكرار الأثر)، وتحوي قاعدة البيانات استرداداً واحداً وصف تدقيق جديداً واحداً. الأدلة: البناء 3.8.0 في بيئة Staging، وحساب الاختبار 5021، والطلب 88412، ومقتطفا السجلين، وQ4 وQ5 مع مخرجاتهما، وCN-1 وCN-4. الخطورة المقترحة: حرج (Critical).

راجع عملك. يجب أن تكون لديك ستة عيوب (F1 إلى F6)، وF7 الأصغر، وطلب API الأول البطيء سؤالاً مفتوحاً، وثلاث نتائج صحيحة على الأقل. وإن لم تجد إلا F1 وF2 فانظر مجدداً في الأعمدة الأخرى للصف المستهدف (F3 إلى F5) وفي نسخة البيانات التي يعرضها التطبيق (F6): فالاستجابة قالت CANCELLED والصفوف الرئيسية بدت سليمة، وهذا بالضبط سبب طلب الدليل أن تتأكد في قاعدة البيانات وأبعد من الصف المستهدف. ولاحظ كيف نُفّذ الاختبار: مرّ الإلغاء عبر التطبيق كما يفعل العميل، ولم تُقرأ قاعدة البيانات إلا بحساب للقراءة فقط؛ أما ضبط status على CANCELLED بعبارة SQL فكان سيتجاوز شيفرة التطبيق التي تعيش فيها معظم هذه العيوب. وبعد الإصلاحات كرّر الاختبار نفسه، مع إعادة المحاولة، اختبارَ تأكيد (Confirmation testing)، واحتفظ بالاستعلامات أعلاه فحوصاً للانحدار.

تقدّمك في هذا الموضوع

جزء من مسار تعلم ضمان الجودة

الأدوات المذكورة في هذا الدليل

فحص جمل SQL بحثاً عن مشكلات الصياغة والأقواس غير المتوازنة والسلاسل النصية غير المغلقة والأنماط الخطرة.

قواعد البيانات

تنسيق استعلامات SQL بحالة أحرف متسقة للكلمات المفتاحية ومسافات بادئة وفواصل أسطر (MySQL وPostgreSQL وSQL Server وOracle وSQLite).

قواعد البيانات

توليد جمل INSERT من تعريف جدول أو صفوف نموذجية أو CSV أو JSON.

قواعد البيانات

تحويل بيانات CSV إلى جمل CREATE TABLE وINSERT مع استنتاج أنواع الأعمدة.

قواعد البيانات

تحويل مصفوفة JSON من الكائنات إلى جمل SQL INSERT (مع CREATE TABLE اختيارياً).

قواعد البيانات

ولّد مدخلات مليئة بالأحرف الخاصة: علامات الاقتباس، والأحرف الوصفية لـ SQL/HTML/JSON، وأحرف التحكم، والرموز التعبيرية، وحيل المسارات.

بيانات الاختبار

ولّد سلاسل نصية اختبارية بترميز Unicode عبر الخطوط (العربية والديفاناغارية وCJK والسيريلية) والرموز التعبيرية وعلامات الدمج وصيغ التطبيع.

بيانات الاختبار

تحويل قائمة قيم (واحدة في كل سطر أو مفصولة بفواصل) إلى عبارة SQL IN (...) مقتبسة.

قواعد البيانات

كيف تساعدك متقن سوفت

أدلة أخرى

أدلة أخرى
قواعد البيانات

SQL للمختبِرين

SQL عملي لمهندسي الجودة: مطابقة الأعداد والمجاميع، وكشف الصفوف اليتيمة والمكررة، وفحص NULL والتواريخ، وفروق اللهجات والممارسة الآمنة.

وقت القراءة 8 دقائق
قواعد البيانات

اختبار المعاملات وترحيلات قواعد البيانات

كيف تختبر معاملات قواعد البيانات وترحيلاتها: الذرّية، ومستويات العزل وظواهرها، والتزامن، وترحيل المخطط والبيانات بأمان.

وقت القراءة 10 دقائق
اختبار واجهات API

ما هو اختبار API؟ وكيف تختبر REST API

شرح اختبار API: ما هو، وما الذي تفحصه في كل طلب واستجابة، وطريقة خطوة بخطوة لاختبار REST API، والأخطاء الشائعة، وأدوات مجانية من متصفحك.

وقت القراءة 6 دقائق
تحليل الأخطاء

كيف تكتب تقرير خطأ (Bug Report)

كيف تكتب تقرير خطأ يستطيع المطورون التصرف بناءً عليه: الحقول المهمة، ونموذج جاهز للنسخ، ومثال كامل، والفرق بين الخطورة والأولوية، والأخطاء الواجب تجنبها.

وقت القراءة 4 دقائق
الأداء

ما هو اختبار الأداء؟

شرح اختبار الأداء: الفرق بين اختبارات الحمل والإجهاد والذروة المفاجئة والتحمّل، والمقاييس المهمة (p95 والإنتاجية ومعدل الأخطاء)، والتخطيط لأول اختبار، والأدوات.

وقت القراءة 5 دقائق
أدوات مساعدة

ما هو UUID؟ الفرق بين v4 وv7 وبين UUID وGUID وULID

شرح UUID: صيغة الـ 128 بت، والفرق بين v4 وv7، ولماذا v7 أفضل لمفاتيح قواعد البيانات، ومقارنة UUID بـ GUID وULID، واحتمال التصادم، والتوليد والتحقق.

وقت القراءة 4 دقائق