التأكيدات في أتمتة الاختبار
التأكيد (Assertion) هو السطر في الاختبار الآلي الذي يقارن النتيجة الفعلية بالنتيجة المتوقعة، ويُفشل الاختبار إذا اختلفتا. فالسطر await expect(page.getByTestId('order-total')).toHaveText('172.50 SAR') تأكيد: إذا عرضت الصفحة 150.00 ريالاً فشل الاختبار وأظهر التقرير القيمتين معاً. الخطوات التي تسبقه تحرّك التطبيق فقط؛ أما التأكيدات فهي ما يحوّل النص البرمجي إلى اختبار.
يتناول هذا الدرس ما الذي تتحقق منه، والتأكيدات الصارمة والمرنة، والتأكيدات التي تعيد المحاولة (Web-first)، ورسائل الفشل. تستخدم الأمثلة Playwright Test مع TypeScript كما في الدروس السابقة، والأفكار نفسها تنطبق على Cypress وJUnit وTestNG وpytest، ونذكر الفروق حيث تهمّ.
لماذا يهم
لا يجد الاختبار إلا العيوب التي تبحث عنها تأكيداته. فاختبار إتمام شراء لا يتحقق إلا من «ظهور إجمالي ما» يبقى ناجحاً حين لا تُضاف ضريبة القيمة المضافة، ويُخبر التشغيلُ الناجح الفريقَ بأن الميزة تعمل. ويسمّي ISTQB النتيجة التي تفوّت عيباً موجوداً نتيجةً سلبية زائفة (False-negative result)، والتأكيدات الضعيفة تُنتج هذه النتائج بصمت.
والخطأ المعاكس مكلف أيضاً. فالتأكيدات التي تقرأ الصفحة مبكراً، أو تتحقق من تفاصيل لم يحددها أحد، تفشل دون أن يكون هناك عيب: نتائج إيجابية زائفة (False-positive results). كل إنذار كاذب يكلّف تحقيقاً، والفريق الذي يرى كثيراً منها يبدأ بإعادة تشغيل الاختبارات الفاشلة بدلاً من قراءة نتائجها. والتأكيدات الدقيقة التي تعيد المحاولة وتحمل رسائل واضحة تُبقي مجموعة الاختبارات جديرة بالثقة: فحين تفشل، تخبرك الرسالة هل تفتح تقرير عيب.
ما الذي تتحقق منه: النتيجة، وبدقة
يحتاج كل تأكيد إلى نتيجة متوقعة مصدرها المرجع (Test Oracle): المتطلب، أو معايير القبول، أو توثيق الواجهة البرمجية، أو حالة الاختبار. ولا تأخذها أبداً مما يعرضه التطبيق اليوم؛ فنسخ المخرجات الحالية إلى التأكيد يحوّل عيباً موجوداً إلى سلوك «متوقع».
التأكيد القوي يتحقق من النتيجة التي يصفها المرجع، وبالدقة التي يحددها. أما التأكيد الضعيف فيتحقق من شيء يبقى صحيحاً حتى حين تكون الميزة معطّلة: أن عنصراً موجود، أو أن قيمة غير فارغة، أو أن الاستجابة «OK». والمثال المستمر في هذا الدرس: منتجان قيمتهما 150.00 ريالاً مع ضريبة القيمة المضافة 15% يجب أن يكون إجماليهما 172.50 ريالاً.
- قيم دقيقة حيث يحددها المتطلب:
toHaveText('172.50 SAR')لاtoBeVisible()على الإجمالي. تُقارن المبالغ والأعداد والحالات والرسائل مقارنةً تامة. - الأنماط فقط للأجزاء المتغيرة فعلاً: رقم طلب مثل
ORD-418305يُتحقق منه بالنمط/^ORD-\d{6}$/، لا بـtoBeTruthy()ولا بالرقم الذي ظهر في تشغيل واحد. - العدد لا «واحد على الأقل»:
toHaveCount(2)لا التحقق من أنcount()أكبر من صفر. - الأثر لا مجرد غياب الخطأ: بعد الطلب تُظهر شارة السلة العدد 0؛ وإذا نصّ توثيق الواجهة البرمجية على أن إنشاء الطلب يعيد
201مع الطلب الجديد في الجسم، فتحقّق من هذا الرمز ومن تلك القيم، لا من أي رمز من فئة 2xx. - الغياب مع مرساة: «لا توجد رسالة خطأ» صحيح أيضاً على صفحة فارغة، فتحقّق أولاً من إشارة إيجابية ثم من الغياب.
- لا أكثر من اللازم: أصناف CSS وبنية الصفحة والطوابع الزمنية والمعرّفات المولَّدة تُفشل الاختبار عند تغييرات غير ضارة. تحقّق من سلوك واحد في كل اختبار، بعدد التأكيدات الذي يلزم لإثباته.
تأكيد ضعيفتنبيه
يتحقق من شيء يبقى صحيحاً حتى لو كانت الميزة معطّلة
- إجمالي الطلب:
isVisible()تعيدtrue: يظهر إجماليٌّ ما- رقم الطلب:
toBeTruthy(): أي نص، حتىORD-- المنتجات المطلوبة:
count()أكبر من صفر- الواجهة البرمجية التي تنشئ الطلب:
res.ok(): أي رمز من فئة 2xx- كيف يقرأ الصفحة:
- مرة واحدة: قد يقرؤها قبل أن تتحدّث
- رسالة الفشل:
Expected: trueReceived: false- العيب: الضريبة لم تُضَف:
- يبقى ناجحاً: يفوته العيبنجاح
تأكيد قوي
يتحقق بدقة من النتيجة التي يحددها المتطلب
- إجمالي الطلب:
toHaveText('172.50 SAR'): الإجمالي الصحيح- رقم الطلب:
/^ORD-\d{6}$/: ORD وستة أرقام- المنتجات المطلوبة:
toHaveCount(2)- الواجهة البرمجية التي تنشئ الطلب:
- الرمز
201والقيم الجديدة في الجسم - كيف يقرأ الصفحة:
- يعيد المحاولة حتى ينجح أو تنتهي المهلة
- رسالة الفشل:
Expected: "172.50 SAR"Received: "150.00 SAR"- العيب: الضريبة لم تُضَف:
- يفشل ويُظهر القيمتينفشل
التحققات مكتوبة بـPlaywright Test، ولها مقابلات في Cypress وJUnit وTestNG وpytest.
التأكيدات الصارمة والمرنة
التأكيد الصارم (Hard Assertion) يوقف الاختبار عند أول فشل: يرمي expect خطأً، ويسجّله مشغّل الاختبارات، ولا تُنفَّذ الأسطر التي تليه أبداً. وهذا هو السلوك الافتراضي في كل أطر العمل، وهو الصحيح حين تعتمد الخطوات اللاحقة على هذا التحقق: فإذا لم تُفتح صفحة اكتمال الطلب، فإن التحقق من إجماليها لا يضيف إلا ضجيجاً.
أما التأكيد المرن (Soft Assertion) فيسجّل الفشل ويترك الاختبار يكمل؛ وفي النهاية يُعلَّم الاختبار فاشلاً مع قائمة بكل تحقق فشل. استخدمه للحقائق المستقلة عن نتيجة واحدة، مثل حقول صفحة واحدة أو استجابة واحدة، فيُظهر تشغيل واحد كل حالات عدم التطابق بدلاً من حالة واحدة في كل دورة إصلاح وإعادة تشغيل. وأبقِ الشروط المسبقة صارمة.
- JUnit 5 وما بعده: تنفّذ
assertAll(...)مجموعة من التأكيدات وتبلّغ عن كل فشل فيها، وتوفّر AssertJ الصنفSoftAssertions. - TestNG: لا يبلّغ
SoftAssertإلا عند استدعاءassertAll()في النهاية؛ وإذا نسيت هذا الاستدعاء ضاعت حالات الفشل. - pytest: عبارة
assertالعادية صارمة، والتحققات المرنة تأتي من إضافات (Plugins).
// Hard: the precondition. If it fails, the checks below would mean nothing.
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
// Soft: independent facts about the same page, all reported together.
await expect.soft(page.getByTestId('order-status')).toHaveText('Paid');
await expect.soft(page.getByTestId('order-item')).toHaveCount(2);
await expect.soft(page.getByTestId('order-total')).toHaveText('172.50 SAR');التأكيدات التي تعيد المحاولة (Web-first)
تتغير الصفحة بعد كل إجراء: ينتهي النقر، ثم يصل الطلب إلى الخادم، وبعدها فقط يتحدّث النص. والتأكيد الذي يقرأ الصفحة مرة واحدة قد يقرؤها مبكراً. أما التأكيدات التي تعيد المحاولة، ويسمّيها توثيق Playwright التأكيدات التي تعيد المحاولة تلقائياً (Auto-retrying) أو Web-first، فتأخذ محدِّداً (Locator) بدلاً من قيمة، وتعيد التحقق حتى يتحقق الشرط أو تنتهي المهلة (5 ثوانٍ افتراضياً في Playwright، وهي قابلة للتعديل). ويعيد Cypress بالطريقة نفسها الاستعلام الذي يسبق .should(). أما Selenium فلا يوفّر تأكيدات تعيد المحاولة: انتظر الشرط باستخدام WebDriverWait ثم تحقّق.
الفرق في موضع await. فالسطر await expect(locator).toHaveText('Paid') يسلّم المحدِّد إلى التأكيد، فيظل يسأل الصفحة. أما expect(await locator.textContent()).toBe('Paid') فيقرأ مرة واحدة ويقارن نصاً لن يتغير أبداً، فتتوقف النتيجة على التوقيت، وهذا سبب شائع للاختبارات المتذبذبة (Flaky Tests). ودوال المطابقة (Matchers) العامة مثل toBe وtoEqual لا تعيد المحاولة من تلقاء نفسها؛ استخدمها للقيم المكتملة أصلاً، مثل جسم استجابة بعد تحليله. وللقيم الموجودة خارج الصفحة تستدعي expect.poll() دالةً مرة بعد مرة، وتطبّق دالة المطابقة على كل نتيجة جديدة.
- تعيد المحاولة في Playwright:
toBeVisibleوtoHaveTextوtoContainTextوtoHaveValueوtoHaveCountعلى المحدِّد، وtoHaveURLوtoHaveTitleعلى الصفحة. - التحققات السلبية تعيد المحاولة أيضاً:
not.toBeVisible()تنتظر اختفاء العنصر، وتنجح فوراً إن لم يظهر أصلاً، ولذلك تحتاج إلى مرساة. - إعادة المحاولة تنتظر الإجابة الصحيحة، لكنها لا تُصلح توقعاً خاطئاً. ارفع المهلة فقط لخطوة معروفة البطء، وعلى ذلك التأكيد وحده. ويتعمّق درس الانتظار والتزامن في هذا الموضوع.
// Reads once: may compare the text from before the server answered
expect(await page.getByTestId('order-status').textContent()).toBe('Paid');
// Retries until the text is "Paid" or the timeout ends (5 s by default)
await expect(page.getByTestId('order-status')).toHaveText('Paid');
// A value outside the page: poll the API until the order is paid
await expect.poll(async () => {
const res = await request.get('/api/orders/' + orderId);
return (await res.json()).status;
}).toBe('PAID');رسائل فشل تشرح نفسها
حين يفشل اختبار في تشغيل ليلي، تكون رسالة الفشل غالباً أول ما يقرؤه أحد. والرسالة المفيدة تجيب عن ثلاثة أسئلة دون فتح الشيفرة: ما الذي جرى التحقق منه، وما المتوقع، وما الذي وصل فعلاً. ودوال المطابقة التي تقارن قيماً حقيقية تعطيك الإجابتين الأخيرتين دون جهد، أما القيمة المنطقية (Boolean) فتُضيّعهما. المخرجات أدناه مختصرة، وشكلها الدقيق يعتمد على إصدار المشغّل.
- فضّل دوال المطابقة التي تحمل القيم (
toHaveTextوtoEqualوtoHaveCount) علىtoBe(true)وtoBeTruthy(). - أضف رسالة مخصصة حيث لا تعرف دالة المطابقة السياق: الوسيط الثاني لـ
expectفي Playwright، والوسيط الأخير لـassertEqualsفي JUnit 5 وما بعده، والنص بعد الفاصلة في pytest:assert total == 172.5, "order total with VAT". - اكتبها كحقيقة من منطق العمل، مثل عنوان تقرير خطأ: «حالة الطلب بعد الدفع بالبطاقة»، لا «التحقق رقم 3». وإذا كرّر الاختبار نفسه على بيانات مختلفة، فضمّن الحالة في الرسالة.
- في الكائنات تطبع
toEqualفرقاً (Diff) بالحقول المختلفة، وتقارنtoMatchObjectالحقول التي تذكرها فقط، فتُبقي الطوابع الزمنية خارج المقارنة.
Weak: expect(await status.isVisible()).toBe(true);
Error: expect(received).toBe(expected)
Expected: true
Received: false
Strong: await expect(status, 'order status after card payment').toHaveText('Paid');
Error: order status after card payment
Expected string: "Paid"
Received string: "Pending"مثال تطبيقي: تقوية اختبار إتمام الشراء
المتطلب: «بعد الدفع تعرض صفحة اكتمال الطلب رقم الطلب (ORD- متبوعاً بستة أرقام)، والحالة Paid، والمنتجات المطلوبة، والإجمالي شاملاً ضريبة القيمة المضافة 15%. وتُفرَّغ السلة». تحتوي سلة الاختبار على منتجين قيمتهما 150.00 ريالاً، فالإجمالي المتوقع 172.50 ريالاً.
- قبل التعديل، يقرأ التحققان الأوّلان مرة واحدة ويقبلان أي شيء: إجمالي 150.00 ريالاً، أو رقم طلب
ORD-بلا أرقام، يبقى ناجحاً. وبعده يُقارن الإجمالي الدقيق ونمط الرقم، مع إعادة المحاولة. - عنوان «Order confirmed» تأكيد صارم: من دون صفحة اكتمال الطلب لا معنى لأي شيء آخر. والحقائق الخمس من المتطلب مرنة، فيبلّغ تشغيل واحد عن كل ما هو خاطئ منها.
- حُذف التحقق من «Error»: كان ينجح على صفحة فارغة، وبعد إثبات العنوان والقيم لا يضيف شيئاً.
- اسم الاختبار الجديد يصف السلوك، فيُقرأ التقرير كالمتطلب. ولا يُتحقق من وقت الطلب ولا من أصناف CSS، لأن المتطلب لا يذكرهما.
// Before: stays green as long as the page opens
test('place order', async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.payWithTestCard();
expect(await page.locator('.total').isVisible()).toBeTruthy();
expect(await page.locator('.order-no').textContent()).toBeTruthy();
await expect(page.getByText('Error')).not.toBeVisible();
});
// After: each assertion proves one part of the requirement
test('card payment confirms order with VAT and empties cart', async ({ page }) => {
const checkout = new CheckoutPage(page);
await checkout.payWithTestCard();
await expect(page.getByRole('heading', { name: 'Order confirmed' })).toBeVisible();
await expect.soft(page.getByTestId('order-number')).toHaveText(/^ORD-\d{6}$/);
await expect.soft(page.getByTestId('order-status')).toHaveText('Paid');
await expect.soft(page.getByTestId('order-item')).toHaveCount(2);
await expect.soft(page.getByTestId('order-total'), 'total = 150.00 + 15% VAT')
.toHaveText('172.50 SAR');
await expect.soft(page.getByTestId('cart-count')).toHaveText('0');
});أخطاء شائعة
- التحقق من الوجود أو من أن القيمة «صادقة» (Truthy)، مثل
toBeVisible()أوtoBeTruthy()أوresponse.ok()، حيث يحدد المتطلب قيمة دقيقة. - نسخ المخرجات الحالية للتطبيق إلى القيمة المتوقعة، فيصبح عيب موجود هو النتيجة المتوقعة.
- قراءة القيمة مرة واحدة بوضع
awaitداخلexpect(...)ومقارنة قيمة ثابتة، بدلاً من تسليم المحدِّد إلى تأكيد يعيد المحاولة؛ فيصبح الاختبار متذبذباً. - تأكيدات سلبية بلا مرساة إيجابية، تنجح على صفحة فارغة أو ما زالت تُحمَّل.
- استخدام التأكيدات المرنة للشروط المسبقة، فيُنتج فشل تسجيل دخول واحد قائمة من حالات فشل بلا معنى؛ أو نسيان
assertAll()في TestNG فلا يُبلَّغ عن حالات الفشل المرنة أبداً. - الإفراط في التحقق من أصناف CSS وبنية الصفحة والطوابع الزمنية والمعرّفات المولَّدة، مما يُفشل الاختبار عند تغييرات غير ضارة ويعلّم الفريق تجاهل التشغيلات الفاشلة.
من منظور ضمان الجودة
عند مراجعة الاختبارات الآلية، اقرأ التأكيدات أولاً. واسأل عن كل تأكيد: أيّ نتيجة متوقعة في حالة الاختبار أو معايير القبول يثبتها؟ وهل سيفشل لو كانت الميزة معطّلة؟ والطريقة البسيطة للتحقق أن تُفشل الاختبار عمداً، بتغيير القيمة المتوقعة أو بتشغيله على بنية (Build) فيها عيب معروف: فالاختبار الذي لم يُرَ فاشلاً قط لم يُثبت أنه قادر على الفشل.
قبل أتمتة أي تحقق، تأكّد من دقة المرجع. فعبارة «تعرض الصفحة الإجمالي» لا تصلح لتأكيد قوي، أما «الإجمالي هو المجموع الفرعي مضافاً إليه ضريبة 15%، مقرّباً إلى منزلتين عشريتين» فتصلح؛ لذا اطلب القاعدة الدقيقة من مالك المنتج. وحين يفشل اختبار، انسخ رسالة التأكيد مع القيمتين المتوقعة والفعلية إلى تقرير الخطأ، ومعها اسم الاختبار وأثر التشغيل (Trace) أو لقطة الشاشة. واتفقوا كفريق على أعراف واضحة: تأكيدات تعيد المحاولة للواجهة، وتأكيدات مرنة للحقائق المجمّعة فقط، ورسائل مكتوبة بلغة منطق العمل.
تمرين عملي
قوِّ تأكيدات اختبار الملف الشخصي
المتطلب: «حين يحفظ مستخدم مسجّل الدخول رقم جوال بصيغة 05 متبوعة بثمانية أرقام، يعيد PATCH /api/profile الرمز 200 والرقم الجديد في phone، وتعرض الصفحة رسالة «Phone number updated»، ويعرض الحقل الرقم المحفوظ».
الاختبار أدناه ينجح اليوم. يبدأ page.waitForResponse(...) الاستماع لاستجابة الملف الشخصي قبل الحفظ، ويعيد await saved تلك الاستجابة حين تصل؛ ويشرح درس الانتظار والتزامن هذا النمط. اشرح لكل تأكيد مرقّم سبب ضعفه أو عدم موثوقيته، ثم أعد كتابة الاختبار بحيث يثبت المتطلب.
test('profile', async ({ page }) => {
const profile = new ProfilePage(page);
await profile.open();
const saved = page.waitForResponse('**/api/profile');
await profile.savePhone('0551234567');
const response = await saved;
expect(response.ok()).toBeTruthy(); // 1
expect(await page.getByRole('status').isVisible()).toBe(true); // 2
expect(await page.getByLabel('Mobile number').inputValue()).toBeTruthy(); // 3
await expect(page.getByText('Something went wrong')).not.toBeVisible(); // 4
});- اكتب النتائج المتوقعة من المتطلب في قائمة: هذه القائمة هي مرجعك.
- لكل سطر مرقّم، سمِّ عيباً يتركه ناجحاً، وبيّن هل يقرأ مرة واحدة أم يعيد المحاولة.
- أعد كتابة كل تأكيد، وقرّر هل يكون صارماً أم مرناً.
- أضف رسالة فشل مخصصة واحدة، وأعد تسمية الاختبار بحسب السلوك.
إظهار الإجابة النموذجية
المرجع: الرمز 200؛ وقيمة phone في الجسم 0551234567؛ ونص الرسالة «Phone number updated»؛ وقيمة الحقل 0551234567.
السطر 1 يتحقق من استجابة مكتملة أصلاً، فلا يحتاج إلى إعادة المحاولة، لكن ok() تكون صحيحة لأي رمز من فئة 2xx: فينجح السطر مع 204، أو مع 200 يحمل جسمها الرقم القديم. والسطر 2 يقرأ مرة واحدة، فقد يُنفَّذ قبل ظهور الرسالة، وينجح أيضاً حين تعرض منطقة الحالة رسالة خطأ. والسطر 3 يقرأ مرة واحدة ويقبل أي قيمة غير فارغة، ومنها الرقم القديم. والسطر 4 يعيد المحاولة، لكن not.toBeVisible() تنجح فوراً على صفحة فارغة أو ما زالت تُحمَّل.
إعادة الكتابة: expect(response.status()).toBe(200) تأكيد صارم، لأن التحققات على الصفحة لا تضيف إلا ضجيجاً إذا فشل الحفظ. ثم تأكيدات مرنة: expect.soft((await response.json()).phone).toBe('0551234567') وawait expect.soft(page.getByRole('status')).toHaveText('Phone number updated') وawait expect.soft(page.getByLabel('Mobile number'), 'mobile number after save').toHaveValue('0551234567'). يستخدم تحققا الاستجابة دوال مطابقة عامة، وهذا صحيح لأن الاستجابة مكتملة أصلاً، ويعيد تحققا الصفحة المحاولة.
يُحذف السطر 4، أو يُنقل بعد التحقق من الرسالة حيث تكون الرسالة مرساته. واسم مناسب للاختبار: saving a valid mobile number updates the profile.
أسئلة شائعة
كم تأكيداً يجب أن يحتوي الاختبار الواحد؟
بقدر ما يلزم لإثبات سلوك واحد. فنصيحة «تأكيد واحد لكل اختبار» تعني سلوكاً واحداً لا سطر expect واحداً: التحقق من حالة الطلب نفسه وإجماليه وعدد منتجاته يجتمع في اختبار واحد. أما إذا تحقق الاختبار من سلوكين لا علاقة بينهما فقسّمه، ليشير اسمه وفشله إلى سبب واحد.
لماذا لا أجعل كل التأكيدات مرنة لأرى كل حالات الفشل مرة واحدة؟
لأن التحققات اللاحقة كثيراً ما تعتمد على السابقة. فإذا فشل تسجيل الدخول، أخفت عشرون حالة فشل مرنة في لوحة التحكم الحالةَ الوحيدة المهمة. اجعل الشروط المسبقة صارمة، واجمع في التأكيدات المرنة الحقائق المستقلة عن نتيجة واحدة فقط.
هل تجعل التأكيدات التي تعيد المحاولة الاختبارات أبطأ؟
ليس حين يكون التطبيق سليماً: فالتأكيد الذي يعيد المحاولة ينجح بمجرد تحقق الشرط، ولا يستهلك المهلة كاملة إلا حين يكون التحقق سيفشل. أما الانتظار الثابت (Sleep) فيكلّف مدته كاملة في كل تشغيل.
المراجع
- Playwright Documentationالتوثيق الرسمي لـ Playwright، إطار الاختبار الشامل الذي يشغّل Chromium وWebKit وFirefox.
- Best Practices — Playwrightاختبر السلوك الذي يراه المستخدم، واجعل الاختبارات معزولة، وفضّل المحدِّدات المتينة والتأكيدات التي تنتظر، واستخدم التتبّع لتصحيح الأخطاء.
- ISTQB Glossaryالتعريفات القياسية لمصطلحات الاختبار — حالة الاختبار واختبار الانحدار والخطورة والأولوية وتحليل القيم الحدّية وغيرها.
- Cypress Documentationالتوثيق الرسمي لاختبار الويب الشامل واختبار المكونات في المتصفح باستخدام Cypress.
تدرّب بأدوات متقن
اختبر التعابير النمطية مباشرةً مع إبراز التطابقات ومجموعات الالتقاط ومعاينة الاستبدال.
مقارنة مستندي JSON دلالياً وإبراز القيم المضافة والمحذوفة والمتغيرة.
قارن نصّين سطراً بسطر أو كلمةً بكلمة مع إبراز الاختلافات.
تقييم تعبيرات JSONPath ($.store.book[*].author) على JSON وعرض النتائج المطابقة فوراً.
اختبر نقاط نهاية REST بتحكم كامل في الطريقة والترويسات وجسم JSON، ثم أضف تأكيدات على الحالة والترويسات ومسارات JSON.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.