الأدلة التقنية

أدوات ضمان الجودة QA: أيها يناسب فريقك؟

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

أدوات ضمان الجودة QA: أيها يناسب فريقك؟

في مقال ضمان الجودة QA صممنا حالات الاختبار Test Cases لميزة استرداد التذاكر، ورأينا أن القيم الحدية Boundary Values وجدول القرار Decision Table وجدا خطأين لم تجدهما الاختبارات العادية. والسؤال الذي يأتي بعد ذلك عملي: بأي أداة تشغل هذه الحالات؟ فإذا بحثت عن «أدوات الاختبار» سوف تجد مئات الأسماء، بعضها يتنافس فعلاً، وبعضها يختبر شيئاً مختلفاً تماماً، وبعضها مجاني على سيرفرك وبعضها اشتراك شهري لكل مستخدم.

وسوف نناقش في هذا المقال ما يلي:

  • خريطة الأدوات حسب ما تختبره: الوحدة والمتصفح وال API والأداء وإدارة حالات الاختبار Test Management وأسلوب BDD والاختبار المرئي والعقود والطفرات وسهولة الوصول Accessibility والجوال.
  • التراخيص وآخر الإصدارات والأسعار كما هي في صفحات الشركات في 12 أكتوبر 2026.
  • تجربة حقيقية لأربع أدوات على تطبيق الاسترداد نفسه: أداة Bruno لاختبار ال API وCucumber للسلوك وأداة axe لسهولة الوصول Accessibility وأداة Stryker للطفرات.
  • جدول قرار يساعدك على الاختيار حسب نوع فريقك.

وبعض هذه العائلات شرحناها بالتفصيل في مقالات أخرى، فمقارنة Playwright وCypress وSelenium في مقال أنواع الاختبارات، ومقارنة k6 وLocust وJMeter وGatling بقياسات حقيقية في مقال اختبار الأحمال، وأدوات جودة الكود في مقالها الخاص، ولن نكرر ذلك هنا:

دليلما هو ضمان الجودة QA وكيف يعمل فريق الاختبار؟كيف تفرق بين QA وQC والاختبار، وتصمم حالات الاختبار Test Cases التي تجد الأخطاء قبل أن يجدها عملاؤك.دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.دليلاختبار الأحمال والضغط Load Testing للمبتدئينلماذا يعمل تطبيقك مع خمسة مستخدمين وينهار مع خمسمئة، وكيف تكتشف ذلك وتصلحه قبل يوم الإطلاق.دليلأدوات جودة الكود: أيها تختار لفريقك؟مقارنة عملية بين مدققات كل لغة والأدوات الجامعة ومنصات الجودة، بتراخيصها وأسعارها ونتائجها على مستودع واحد.

خريطة الأدوات: كل عائلة تختبر طبقة

أكثر خطأ نراه عند اختيار الأدوات أن الفريق يقارن أداة بأداة لا تنافسها، مثل Postman وPlaywright، أو يظن أن أداة إدارة الاختبارات مثل TestRail تختبر شيئاً بنفسها. والصورة التالية تبين العائلات وأين تنظر كل واحدة في التطبيق:

خريطة حول مربع التطبيق بواجهته وال API وقاعدة البيانات: اختبار الوحدة pytest وJUnit وJest وVitest، واختبار الطفرات Stryker وPIT وmutmut، وإدارة الاختبارات Kiwi TCMS وTestLink وQase وTestRail، والمتصفح Playwright وCypress وSelenium، وال API والعقود Bruno وPostman وHoppscotch وKarate وPact، والأداء k6 وJMeter وGatling وLocust، و BDD والاختبار المرئي Cucumber وBackstopJS، وسهولة الوصول axe وLighthouse، والجوال Appium وMaestro
كل عائلة ترى طبقة مختلفة من التطبيق، وأدوات الإدارة تجمع النتائج ولا تختبر

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

أطر اختبار الوحدة Unit لكل لغة

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

اللغةالإطارالترخيصآخر إصدار في أكتوبر 2026
لغة Pythonأداة pytestرخصة MITالإصدار 9.1.1
لغة Javaإطار JUnitرخصة EPL-2.0الإصدار 6.1.3
لغة Kotlinإطار Kotestرخصة Apache-2.0الإصدار 6.2.5
لغة JavaScript وTypeScriptأداة Vitest وأداة Jestرخصة MIT للاثنتينالإصدار 5.0.4 والإصدار 30.5.2
لغة Goالحزمة testingرخصة BSD-3-Clauseجزء من Go نفسها
لغة C# و.NETإطار xUnit.net وإطار NUnitرخصة Apache-2.0 ورخصة MITالنسخة v3 من الأول، والإصدار 5.0.0 من الثاني
لغة PHPإطار PHPUnitرخصة BSD-3-Clauseالإصدار 13.4.2
لغة Rubyإطار RSpecرخصة MITالإصدار 3.13.2

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

اختبار المتصفح E2E مع Playwright وCypress وSelenium

قارنا الثلاثة بنقاط القوة والضعف في مقال أنواع الاختبارات، والجديد هنا الإصدارات والتكلفة:

  • أداة Playwright: بترخيص Apache-2.0، وآخر إصدار 1.64.0، ولا توجد خدمة مدفوعة إلزامية، فالتوازي والتقارير وعارض التتبع كلها داخل الأداة.
  • أداة Cypress: بترخيص MIT، وآخر إصدار 16.1.1، أما خدمة Cypress Cloud للتسجيلات والتوازي الموزع فمجانية حتى 500 نتيجة اختبار في الشهر، وخطة Team بسعر 67 دولاراً في الشهر بالدفع السنوي 799 دولاراً، وخطة Business بسعر 267 دولاراً في الشهر بالدفع السنوي 3199 دولاراً.
  • أداة Selenium: بترخيص Apache-2.0، وآخر إصدار 4.51.0، ومجانية بالكامل بما فيها Selenium Grid الذي تستضيفه بنفسك.

ولمشروع جديد في 2026 نختار Playwright للأسباب التي شرحناها هناك، ونبقى على Selenium عندما يكون عند الفريق مكتبة اختبارات كبيرة مكتوبة به، أو يحتاج متصفحات حقيقية مثبتة عبر WebDriver.

أدوات اختبار ال API مثل Bruno وPostman وHoppscotch وKarate

أدوات ال API تبدأ عادة كعميل HTTP بواجهة رسومية يجرب فيه المطور الطلبات، ثم تصبح أداة اختبار عندما تضيف إليها التحققات Assertions وتشغلها من سطر الأوامر في CI. والفرق الأهم بينها ليس الواجهة، وإنما أين تحفظ الطلبات: في ملفات نصية داخل المستودع تراجعها مع الكود، أم في سحابة الشركة.

أداة Bruno

أداة Bruno بترخيص MIT، وآخر إصدار 4.2.1، وتحفظ كل طلب في ملف نصي بامتداد .bru داخل مستودعك، فتراجع تغيير الطلبات في طلب الدمج كما تراجع الكود، وأداة سطر الأوامر @usebruno/cli تشغل المجموعة في CI. وتذكر صفحة الأسعار أن النسخة المفتوحة مجانية، وأن خطة Pro بسعر 6 دولارات للمستخدم شهرياً، وخطة Ultimate بسعر 11 دولاراً، وكلاهما بالدفع السنوي فقط، وتضيفان مثلاً مزودي Git الخاصين وتسجيل الدخول الموحد SSO.

نقاط القوة: الملفات في Git، ولا يوجد حساب إجباري، وأداة سطر الأوامر مجانية.

نقاط الضعف: أحدث من Postman، فالأمثلة والتكامل مع الأدوات الأخرى أقل، وبعض ميزات الفرق في الخطط المدفوعة.

أداة Postman

تطبيق Postman مغلق المصدر، وأداة تشغيل مجموعاته من سطر الأوامر Newman بترخيص Apache-2.0. ولاحظ أن صفحة الأسعار تغيرت، فالخطط اليوم هي Free لمستخدم واحد، وSolo بسعر 9 دولارات في الشهر بالدفع السنوي، وTeam بسعر 19 دولاراً للمستخدم في الشهر بالدفع السنوي، وEnterprise بسعر خاص، والصفحة نفسها تقول إن خطتي Basic وProfessional لم تعودا متاحتين للعملاء الجدد، فإذا قرأت مقارنة قديمة تذكرهما فأسعارها لم تعد صحيحة.

نقاط القوة: الأشهر، وفيه مراقبة Monitors وتوثيق ومحاكاة Mock Servers في مكان واحد.

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

أداة Hoppscotch

أداة Hoppscotch بترخيص MIT، وآخر إصدار 2026.9.0، وهي واجهة ويب يمكنك استضافتها على سيرفرك، ومعها أداة سطر أوامر. وتذكر صفحة الأسعار خطة مجانية، وخطة Business بسعر 6 دولارات للمستخدم شهرياً، وEnterprise بسعر 11 دولاراً، بالدفع السنوي.

نقاط القوة: تعمل من المتصفح دون تثبيت، وتستضاف على سيرفرك، وتدعم REST وGraphQL وWebSocket.

نقاط الضعف: التشغيل في CI أقل نضجاً من Bruno وNewman.

إطار Karate

إطار Karate بترخيص MIT، وآخر إصدار 2.1.3، ويختلف عن الثلاثة السابقة بأنه إطار اختبار وليس عميل HTTP، فتكتب الاختبارات بلغة قريبة من Gherkin، ويعمل على JVM، ويجمع اختبار ال API والأداء عبر Gatling. وشركة Karate Labs تبيع إضافات مثل IDE Plus بسعر 100 دولار للمستخدم سنوياً وPro بسعر 640 دولاراً كما في صفحة الأسعار.

نقاط القوة: اختبارات API مقروءة دون كود Java، وتحققات قوية على JSON.

نقاط الضعف: يحتاج JVM، ولغته الخاصة شيء جديد يتعلمه الفريق.

أدوات الأداء: k6 وJMeter وGatling وLocust

شغلنا الأربعة على السيناريو نفسه وقارنا نتائجها في مقال اختبار الأحمال، وهنا التراخيص والتكلفة فقط:

الأداةالترخيصآخر إصدارملاحظة
أداة k6رخصة AGPL-3.0الإصدار 2.3.0خدمة Grafana Cloud k6 مجانية حتى 500 ساعة مستخدم افتراضي VUh في الشهر، ثم تبدأ من 0.15 دولار للساعة في خطة Pro
أداة Apache JMeterرخصة Apache-2.0الإصدار 5.6.3 من يناير 2024تعمل على Java، ولها واجهة رسومية لبناء الاختبار
أداة Gatlingرخصة Apache-2.0الإصدار 3.16.0خدمة Gatling Enterprise تبدأ من 99 يورو في الشهر
أداة Locustرخصة MITالإصدار 2.46.7تكتب السيناريو بلغة Python

ولاحظ أن ترخيص AGPL الخاص بأداة k6 لا يؤثر عليك إذا شغلتها لاختبار تطبيقك، وإنما يهم من يعدلها ويقدمها كخدمة للآخرين.

إدارة حالات الاختبار Test Management

هذه العائلة لا تختبر شيئاً، وإنما تحفظ حالات الاختبار Test Cases وخطط الاختبار وجولات التشغيل Test Runs ونتائجها، وتربط كل حالة بالمتطلب وبالخطأ الذي كشفته، وتعطي المدير تقرير «ماذا اختبرنا في هذا الإصدار؟». وتحتاجها عندما يكون عندك اختبار يدوي كثير، أو عميل أو جهة رقابية تطلب دليلاً على ما اختبر، أما الفريق الذي كل اختباراته آلية في CI فتكفيه غالباً تقارير CI نفسها.

الأداةالترخيص والاستضافةالسعر في أكتوبر 2026
أداة Kiwi TCMSرخصة GPL-2.0، وتستضيفها على سيرفرك، وآخر إصدار 16.6مجانية على سيرفرك، ولها اشتراكات دعم تبدأ من 35 دولاراً في الشهر، واستضافة مدارة بسعر 2000 دولار في الشهر
أداة TestLinkرخصة GPL-2.0، وتستضيفها على سيرفركمجانية، ولكن آخر تغيير في مستودعها كان في ديسمبر 2025
خدمة Qaseسحابيةمجانية حتى 4 مستخدمين ومشروعين، وخطة Teams بسعر 35 دولاراً للمستخدم شهرياً بالدفع السنوي أو 42 دولاراً بالدفع الشهري
خدمة TestRailسحابية، أو على سيرفرك من 10 مستخدمينخطة Essentials بسعر 22 دولاراً للمستخدم شهرياً، وProfessional بسعر 38 دولاراً، وEnterprise بسعر 78 دولاراً، بالدفع السنوي

نقاط القوة: تاريخ كامل لما اختبر ومن اختبره، وربط بالمتطلبات والأخطاء، واستيراد نتائج الاختبارات الآلية بصيغة JUnit XML.

نقاط الضعف: الحالات المكتوبة يدوياً تتقادم بسرعة إذا لم يحدثها أحد، والأدوات السحابية تحسب لكل مستخدم، فتصبح مكلفة عندما يحتاج المطورون كلهم حسابات. وإذا كنت تبدأ اليوم على سيرفرك فنفضل Kiwi TCMS على TestLink، والسبب أن الأولى أصدرت نسختها 16.6 في أكتوبر 2026، والثانية لم يتغير مستودعها منذ ديسمبر 2025.

أسلوب BDD مع Cucumber

أسلوب التطوير بالسلوك Behavior-Driven Development، واختصاراً BDD، يكتب القواعد كأمثلة بلغة يقرؤها مالك المنتج، بصيغة Gherkin المعروفة: Given وWhen وThen، ثم يربط كل سطر منها بدالة في الكود. والأداة الأشهر Cucumber بترخيص MIT، ولها نسخة JavaScript بالإصدار 13.3.0 ونسخة Java، وفي Python تستخدم غالباً behave بترخيص BSD-2-Clause. ولغة Gherkin تدعم الكلمات المفتاحية العربية مثل «خاصية» و«سيناريو» و«بفرض» و«عندما» و«ثم» كما في قائمة اللغات الرسمية.

نقاط القوة: القواعد تصبح وثيقة حية تختبر نفسها، وجدول الأمثلة Examples مكان طبيعي للقيم الحدية Boundary Values.

نقاط الضعف: إذا لم يقرأ مالك المنتج ملفات Gherkin فعلاً فهي طبقة إضافية بلا فائدة فوق اختبارات عادية، وهذا ما يحدث في كثير من الفرق.

الاختبار المرئي Visual Regression

شرحنا فكرته في مقال أنواع الاختبارات، وأسهل بداية هي التحقق toHaveScreenshot() المدمج في Playwright. أما الأدوات المفتوحة المخصصة فأغلبها تباطأ، فآخر إصدار من BackstopJS على npm كان في سبتمبر 2024، وآخر إصدار من Lost Pixel في نوفمبر 2024، وكلاهما بترخيص MIT. والخدمات السحابية تقارن الصور على سيرفراتها وتعرض الفروق للمراجعة:

  • خدمة Percy من BrowserStack: مجانية حتى 5000 لقطة في الشهر، وخطة Desktop بسعر 199 دولاراً في الشهر بالدفع السنوي.
  • خدمة Chromatic: مجانية حتى 5000 لقطة، وخطة Starter بسعر 179 دولاراً في الشهر.
  • خدمة Applitools: خطة Starter بسعر 667 دولاراً في الشهر بالدفع السنوي، والخطط الأكبر بسعر خاص.

اختبار العقود Contract Testing مع Pact

شرحنا الفكرة في قسم اختبار العقد. ومكتبات Pact مفتوحة: pact-js بترخيص MIT بالإصدار 17.1.4، وpact-jvm بترخيص Apache-2.0، وPact Broker الذي يحفظ العقود بترخيص MIT وتستضيفه بنفسك. أما خدمة PactFlow من SmartBear فمجانية حتى تكاملين، وخطة Team بسعر 127 دولاراً في الشهر أو 1385 دولاراً في السنة. ولا تحتاج هذه العائلة إلا عندما تملك فرق مختلفة خدمات تتواصل عبر ال API.

اختبار الطفرات Mutation Testing

أداة الطفرات تغير الكود عمداً، مثل >= إلى >، ثم تشغل الاختبارات، فإذا نجحت الاختبارات مع الكود المغير فهي لا تتحقق من هذا السطر فعلاً، وقد استخدمنا mutmut في مقال الاختبارات بالذكاء الاصطناعي. والأدوات حسب اللغة:

  • أداة StrykerJS بترخيص Apache-2.0 والإصدار 10.0.0 للغة JavaScript وTypeScript، ومعها Stryker.NET بالإصدار 5.0.0.
  • أداة PIT بترخيص Apache-2.0 والإصدار 1.30.0 للغة Java، ولها إضافات تجارية من Arcmutate لا تنشر الشركة أسعارها.
  • أداة mutmut بترخيص BSD-3-Clause والإصدار 3.8.0 للغة Python.

سهولة الوصول Accessibility

المكتبة الأساسية هي axe-core بترخيص MPL-2.0 والإصدار 4.14.0، وتفحص الصفحة حسب قواعد WCAG، وتستخدمها أدوات كثيرة من داخلها، ومنها الحزمة @axe-core/playwright التي سوف نستخدمها في التجربة. ومعها Lighthouse بترخيص Apache-2.0 الذي يعطي درجة لسهولة الوصول Accessibility مع الأداء، وPa11y بترخيص LGPL-3.0 لفحص قائمة صفحات من سطر الأوامر. أما axe DevTools التجاري فإضافة المتصفح منه مجانية، والنسخ المدفوعة بلا سعر منشور.

ولاحظ أن الأدوات الآلية تجد جزءاً فقط من مشاكل سهولة الوصول Accessibility، وسوف ترى مثالاً على ذلك في التجربة.

اختبار تطبيقات الجوال

  • أداة Appium: بترخيص Apache-2.0 والإصدار 3.8.0، وتتحكم في تطبيقات Android وiOS عبر بروتوكول WebDriver نفسه الذي يستخدمه Selenium، فتكتب الاختبارات بأي لغة.
  • أداة Maestro: بترخيص Apache-2.0 والإصدار 2.11.0، وتكتب الاختبار بملف YAML قصير، وهي أسهل بداية، وخدمة Maestro Cloud لتشغيلها على أجهزة سحابية بسعر 250 دولاراً للجهاز في الشهر.
  • أطر المنصة نفسها: Espresso من Google لنظام Android وXCTest من Apple لنظام iOS، وهي الأسرع والأكثر استقراراً، ولكن كل واحد لمنصة واحدة وبلغتها.

تجربة عملية: أربع أدوات على تطبيق واحد

نأخذ قواعد الاسترداد من تجربة مقال ضمان الجودة بعد إصلاحها، ونكتبها بلغة JavaScript في تطبيق Node.js صغير له صفحة ويب و API، ثم نشغل عليه أربع أدوات، كل واحدة في Container منفصل على شبكة lab-qa-net، وكل Container يبدأ اسمه بالبادئة lab-qa-. ووضعنا في التطبيق عن قصد خطأ في ال API ومشكلتين في الصفحة، والصورة التالية تبين ما تنظر إليه كل أداة:

تطبيق lab-qa-app في الوسط بصفحة ويب و API لحساب الاسترداد والملف refund.js، وحوله أربع أدوات: Bruno CLI يفحص ال API ووجد رمز حالة 200 بدل 400، وCucumber يشغل القواعد كأمثلة ونجحت ستة سيناريوهات، وaxe مع Playwright يفحص الصفحة ووجد ضعف التباين وقائمة بلا اسم، وStryker يغير الملف refund.js ورفع درجة الطفرات من 87.80% إلى 100%
كل أداة رأت طبقة مختلفة، ولم تجد أي واحدة منها ما وجدته الأخرى

التطبيق

شكل المجلد كما يلي:

lab/
├── compose.yaml
├── app/
│   ├── package.json
│   ├── refund.js
│   ├── server.js
│   ├── stryker.config.json
│   └── test/refund.test.js
├── bruno/refunds/
│   ├── bruno.json
│   ├── environments/local.bru
│   ├── vip-under-24h.bru
│   ├── cancelled-promo.bru
│   └── unknown-ticket.bru
├── bdd/
│   ├── package.json
│   └── features/
│       ├── refund.feature
│       └── step_definitions/steps.mjs
└── a11y/
    ├── package.json
    ├── playwright.config.ts
    └── tests/a11y.spec.ts

الملف app/refund.js هو القواعد نفسها بعد الإصلاح:

// Ticket refund policy, same rules as the qa-intro article (fixed version).
export const TYPES = ['standard', 'vip', 'promo'];
const HOUR = 3600 * 1000;

export function refundPercent(ticketType, eventStart, requestedAt, cancelled = false) {
  if (!TYPES.includes(ticketType)) throw new Error(`unknown ticket type: ${ticketType}`);
  if (cancelled) return 100;
  if (ticketType === 'promo') return 0;
  const left = eventStart - requestedAt; // milliseconds
  if (left <= 0) return 0;
  if (ticketType === 'vip') return left >= 24 * HOUR ? 100 : 50;
  if (left >= 72 * HOUR) return 100;
  if (left >= 24 * HOUR) return 50;
  return 0;
}

والملف app/server.js يقدم الصفحة والمسار POST /api/refunds/quote دون أي مكتبة خارجية، وهذا الجزء المهم منه، ولاحظ أن الخطأ يرجع برمز الحالة 200:

  if (req.method === 'POST' && req.url === '/api/refunds/quote') {
    let raw = '';
    req.on('data', (c) => (raw += c));
    req.on('end', () => {
      try {
        const { ticket, hoursLeft, cancelled = false } = JSON.parse(raw);
        const now = Date.now();
        const percent = refundPercent(ticket, now + hoursLeft * 3600 * 1000, now, cancelled);
        json(res, 200, { ticket, hoursLeft, cancelled, percent });
      } catch (err) {
        json(res, 200, { error: err.message });
      }
    });
    return;
  }

وفي الصفحة نفسها نص إرشادي بلون رمادي فاتح #b9b9b9، وقائمة اختيار نوع التذكرة وحقل الساعات دون عنصر label، والحقل له نص placeholder فقط:

  <p class="hint">Enter how many hours are left before the event.</p>
  <form id="quote">
    <select id="ticket"><option>standard</option><option>vip</option><option>promo</option></select>
    <input id="hours" type="number" placeholder="Hours left">
    <button type="submit">Quote</button>
  </form>

ونشغل التطبيق بملف compose.yaml على Docker Image الرسمية لـ Node.js دون بناء:

name: lab-qa
services:
  app:
    image: node:24-alpine
    container_name: lab-qa-app
    working_dir: /app
    volumes:
      - ./app:/app:ro
    command: ["node", "server.js"]
    ports:
      - "127.0.0.1:19380:3000"
networks:
  default:
    name: lab-qa-net
docker compose up -d
curl -s -XPOST 127.0.0.1:19380/api/refunds/quote -d '{"ticket":"vip","hoursLeft":10}'
{"ticket":"vip","hoursLeft":10,"cancelled":false,"percent":50}

الأداة الأولى: أداة Bruno تختبر ال API

كل طلب في ملف، وهذا الملف unknown-ticket.bru الذي يرسل نوع تذكرة غير موجود، ويتوقع الرمز 400 ورسالة واضحة:

meta {
  name: Unknown ticket type is rejected
  type: http
  seq: 3
}

post {
  url: {{baseUrl}}/api/refunds/quote
  body: json
  auth: none
}

body:json {
  { "ticket": "gold", "hoursLeft": 10 }
}

assert {
  res.status: eq 400
  res.body.error: contains unknown ticket type
}

والملفان الآخران بالشكل نفسه، الأول لتذكرة مميزة بقي لها 10 ساعات ويتوقع 50%، والثاني لتذكرة ترويجية في فعالية ملغاة ويتوقع 100%، والمتغير baseUrl في الملف environments/local.bru قيمته http://lab-qa-app:3000. نشغل المجموعة بأداة سطر الأوامر:

docker run --rm --name lab-qa-bruno --network lab-qa-net \
  -v "$PWD/bruno/refunds:/refunds" -w /refunds node:24-alpine \
  sh -c "npm i -g @usebruno/[email protected] >/dev/null 2>&1; bru run --env local"
vip-under-24h (200 OK) - 24 ms
Assertions
   ✓ res.status: eq 200
   ✓ res.body.percent: eq 50
cancelled-promo (200 OK) - 4 ms
Assertions
   ✓ res.status: eq 200
   ✓ res.body.percent: eq 100
unknown-ticket (200 OK) - 3 ms
Assertions
   ✕ res.status: eq 400
      expected 200 to equal 400
   ✓ res.body.error: contains unknown ticket type

📊 Execution Summary
┌───────────────┬────────────────────────┐
│ Metric        │         Result         │
├───────────────┼────────────────────────┤
│ Status        │         ✗ FAIL         │
├───────────────┼────────────────────────┤
│ Requests      │ 3 (2 Passed, 1 Failed) │
├───────────────┼────────────────────────┤
│ Tests         │          0/0           │
├───────────────┼────────────────────────┤
│ Assertions    │          5/6           │
├───────────────┼────────────────────────┤
│ Duration (ms) │          263           │
└───────────────┴────────────────────────┘

الرسالة صحيحة، ولكن الرمز 200 يعني لأي برنامج يستدعي ال API أن الطلب نجح، فالتطبيق الذي يقرأ percent سوف يجده فارغاً. وهذا الخطأ لا تراه اختبارات الوحدة لأن الدالة نفسها ترمي الاستثناء بشكل صحيح، ولا يراه من يجرب الصفحة لأنها تعرض الرسالة. بعد تغيير الرمز في server.js إلى 400 وإعادة تشغيل التطبيق، وهذا آخر المخرج:

unknown-ticket (400 Bad Request) - 5 ms
Assertions
   ✓ res.status: eq 400
   ✓ res.body.error: contains unknown ticket type

📊 Execution Summary
┌───────────────┬──────────────┐
│ Metric        │    Result    │
├───────────────┼──────────────┤
│ Status        │    ✓ PASS    │
├───────────────┼──────────────┤
│ Requests      │ 3 (3 Passed) │
├───────────────┼──────────────┤
│ Tests         │     0/0      │
├───────────────┼──────────────┤
│ Assertions    │     6/6      │
├───────────────┼──────────────┤
│ Duration (ms) │     249      │
└───────────────┴──────────────┘

الأداة الثانية: أداة Cucumber تشغل القواعد كأمثلة

الملف features/refund.feature يكتب القواعد بلغة يقرؤها مالك المنتج، وجدول الأمثلة Examples فيه القيم الحدية Boundary Values من المقال السابق، فالقيمة 71.99 ساعة هي 71 ساعة و59 دقيقة تقريباً:

Feature: Ticket refund quote
  The customer asks how much of the ticket price comes back
  before deciding to cancel.

  Scenario Outline: Refund depends on the ticket type and the time left
    Given a "<ticket>" ticket
    And the event starts in <hours> hours
    When the customer asks for a refund quote
    Then the refund is <percent>%

    Examples:
      | ticket   | hours | percent |
      | standard | 72    | 100     |
      | standard | 71.99 | 50      |
      | standard | 23.99 | 0       |
      | vip      | 24    | 100     |
      | vip      | 1     | 50      |

  Scenario: The organiser cancels the event
    Given a "promo" ticket
    And the event starts in 5 hours
    And the organiser has cancelled the event
    When the customer asks for a refund quote
    Then the refund is 100%

وكل سطر مربوط بدالة في features/step_definitions/steps.mjs، والدوال ترسل طلباً حقيقياً إلى ال API:

import assert from 'node:assert/strict';
import { Given, When, Then } from '@cucumber/cucumber';

const BASE = process.env.BASE_URL ?? 'http://lab-qa-app:3000';

Given('a {string} ticket', function (ticket) {
  this.request = { ticket, cancelled: false };
});

Given('the event starts in {float} hours', function (hours) {
  this.request.hoursLeft = hours;
});

Given('the organiser has cancelled the event', function () {
  this.request.cancelled = true;
});

When('the customer asks for a refund quote', async function () {
  const res = await fetch(`${BASE}/api/refunds/quote`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(this.request),
  });
  this.response = await res.json();
});

Then('the refund is {int}%', function (percent) {
  assert.equal(this.response.percent, percent);
});
docker run --rm --name lab-qa-bdd --network lab-qa-net -v "$PWD/bdd:/bdd" -w /bdd node:24-alpine \
  sh -c "npm install --silent >/dev/null 2>&1; npx cucumber-js --import 'features/**/*.mjs' --format progress"
.........................

6 scenarios (6 passed)
25 steps (25 passed)
0m 0.202s (0m 0.148s executing your code)

والآن نكسرها: لنفرض أن مالك المنتج أضاف قاعدة جديدة في ملف gift-card.feature قبل أن يكتب المطور كودها:

Feature: Gift card refunds

  Scenario: A ticket paid with a gift card
    Given a "standard" ticket
    And the event starts in 100 hours
    And the customer paid with a gift card
    When the customer asks for a refund quote
    Then the refund goes back to the gift card
..U-U

Undefined scenarios:
  1) A ticket paid with a gift card # features/gift-card.feature:3
       And the customer paid with a gift card
       Then the refund goes back to the gift card

1 scenario (1 undefined)
5 steps (2 passed, 1 skipped, 2 undefined)
0m 0.043s (0m 0.001s executing your code)

You can implement missing steps with the snippets below:

Given('the customer paid with a gift card', function () {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});

Then('the refund goes back to the gift card', function () {
  // Write code here that turns the phrase above into concrete actions
  return 'pending';
});

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

الأداة الثالثة: أداة axe تفحص سهولة الوصول Accessibility

نشغل axe من داخل اختبار Playwright، ونطلب قواعد WCAG 2.0 و2.1 بمستويي A وAA، والاختبار يطبع كل مشكلة والعنصر الذي فيه:

import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('refund page has no WCAG A/AA violations', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
    .analyze();

  for (const v of results.violations) {
    console.log(`${v.id} [${v.impact}] ${v.help}`);
    for (const node of v.nodes) console.log(`   ${node.target.join(' ')}`);
  }
  expect(results.violations.map((v) => v.id)).toEqual([]);
});

الملف package.json يثبت @playwright/test بالإصدار 1.64.0 و@axe-core/playwright بالإصدار 4.13.0، ونشغل الاختبار في Docker Image الرسمية لـ Playwright بالإصدار نفسه:

docker run --rm --name lab-qa-a11y --init --ipc=host --network lab-qa-net \
  -v "$PWD/a11y:/a11y" -w /a11y mcr.microsoft.com/playwright:v1.64.0-noble \
  sh -c "npm install --silent >/dev/null 2>&1; npx playwright test"
Running 1 test using 1 worker

color-contrast [serious] Elements must meet minimum color contrast ratio thresholds
   .hint
select-name [critical] Select element must have an accessible name
   #ticket
  ✘  1 tests/a11y.spec.ts:4:5 › refund page has no WCAG A/AA violations (737ms)

  1) tests/a11y.spec.ts:4:5 › refund page has no WCAG A/AA violations ──────────────────────────────

    Error: expect(received).toEqual(expected) // deep equality

    - Expected  - 1
    + Received  + 4

    - Array []
    + Array [
    +   "color-contrast",
    +   "select-name",
    + ]

في المخرج أعلاه لاحظ التالي:

  • النص الرمادي تباينه مع الخلفية البيضاء 1.96 كما ذكرت axe في التفاصيل الكاملة، والمطلوب في WCAG بمستوى AA هو 4.5 للنص العادي.
  • قائمة نوع التذكرة بلا اسم، فقارئ الشاشة Screen Reader سوف يقول «قائمة» فقط، ولا يعرف المستخدم الكفيف ماذا يختار.
  • حقل الساعات لم يظهر في النتائج رغم أنه بلا label، والسبب أن axe يقبل نص placeholder كاسم للحقل، مع أن هذا النص يختفي بمجرد أن يبدأ المستخدم الكتابة. وهذا مثال على ما قلناه أعلاه: الأداة الآلية تجد جزءاً من المشاكل، والباقي يحتاج مراجعة إنسان.

بعد إضافة عنصري label للقائمة والحقل، وتغيير اللون إلى #595959:

Running 1 test using 1 worker

  ✓  1 tests/a11y.spec.ts:4:5 › refund page has no WCAG A/AA violations (1.4s)

  1 passed (3.5s)

الأداة الرابعة: أداة Stryker تختبر الاختبارات

في app/test/refund.test.js كتبنا بأداة Vitest نفس مجموعة الفئات المتكافئة Equivalence Partitions من المقال السابق، أي قيمة واحدة من كل فئة مع اختبار الإلغاء والنوع غير الصالح، وعشرة اختبارات ناجحة. وملف الإعداد stryker.config.json يحدد الملف الذي نغيره:

{
  "testRunner": "vitest",
  "mutate": ["refund.js"],
  "reporters": ["clear-text", "progress"],
  "coverageAnalysis": "perTest"
}
docker run --rm --name lab-qa-mutation -v "$PWD/app:/app" -w /app node:24-alpine \
  sh -c "npm install --silent >/dev/null 2>&1; npx stryker run --reporters clear-text"

وهنا واجهنا مشكلة تستحق أن تعرفها: مع Vitest بالإصدار 5.0.4 وStryker بالإصدار 10.0.0 خرجت النتيجة 12.20%، وفي قائمة الطفرات الناجية طفرات واضح أنها يجب أن تموت، مثل تغيير if (left >= 24 * HOUR) return 50; إلى if (true) return 50; مع أن اختبار العشر ساعات يتوقع 0%. والإصدار 5.0.0 من Vitest صدر في 3 سبتمبر 2026، أي بعد Stryker 10.0.0 الذي صدر في 14 أغسطس، فعدنا إلى Vitest 4.1.11 وخرجت النتيجة الصحيحة:

[Survived] StringLiteral
refund.js:6:52
-     if (!TYPES.includes(ticketType)) throw new Error(`unknown ticket type: ${ticketType}`);
+     if (!TYPES.includes(ticketType)) throw new Error(``);
--
[Survived] EqualityOperator
refund.js:10:7
-     if (left <= 0) return 0;
+     if (left < 0) return 0;
--
[Survived] EqualityOperator
refund.js:11:36
-     if (ticketType === 'vip') return left >= 24 * HOUR ? 100 : 50;
+     if (ticketType === 'vip') return left > 24 * HOUR ? 100 : 50;
--
[Survived] EqualityOperator
refund.js:12:7
-     if (left >= 72 * HOUR) return 100;
+     if (left > 72 * HOUR) return 100;
--
[Survived] EqualityOperator
refund.js:13:7
-     if (left >= 24 * HOUR) return 50;
+     if (left > 24 * HOUR) return 50;

-----------|------------------|----------|-----------|------------|----------|----------|
           | % Mutation score |          |           |            |          |          |
File       |  total | covered | # killed | # timeout | # survived | # no cov | # errors |
-----------|--------|---------|----------|-----------|------------|----------|----------|
All files  |  87.80 |   87.80 |       36 |         0 |          5 |        0 |        0 |
 refund.js |  87.80 |   87.80 |       36 |         0 |          5 |        0 |        0 |
-----------|--------|---------|----------|-----------|------------|----------|----------|

أربع من الطفرات الخمس الناجية هي تغيير >= إلى > أو <= إلى <، أي أن Stryker وجد بنفسه، دون أن يعرف شيئاً عن القواعد، أن الاختبارات لا تلمس الحدود، وهي النتيجة نفسها التي وصلنا إليها في المقال السابق بتصميم القيم الحدية Boundary Values يدوياً. والطفرة الخامسة تقول إن أحداً لا يتحقق من نص رسالة الخطأ. فنضيف مجموعة ثانية:

// Boundary values: exactly on each edge.
describe('refundPercent: boundaries', () => {
  it.each([
    ['standard', 72, 100],
    ['standard', 24, 50],
    ['vip', 24, 100],
    ['vip', 0, 0],
  ])('%s, exactly %d hours left -> %d%%', (ticket, hours, expected) => {
    expect(refundPercent(ticket, START, before(hours))).toBe(expected);
  });

  it('names the unknown ticket type in the error', () => {
    expect(() => refundPercent('gold', START, before(10))).toThrow(/unknown ticket type: gold/);
  });
});
-----------|------------------|----------|-----------|------------|----------|----------|
           | % Mutation score |          |           |            |          |          |
File       |  total | covered | # killed | # timeout | # survived | # no cov | # errors |
-----------|--------|---------|----------|-----------|------------|----------|----------|
All files  | 100.00 |  100.00 |       41 |         0 |          0 |        0 |        0 |
 refund.js | 100.00 |  100.00 |       41 |         0 |          0 |        0 |        0 |
-----------|--------|---------|----------|-----------|------------|----------|----------|

ولاحظ أن تشغيل Stryker استغرق بين 43 و55 ثانية على دالة من 15 سطراً، لأنه يشغل الاختبارات مرة لكل طفرة، لذلك يناسبه أن يعمل على الملفات المهمة أو على الكود الجديد في طلب الدمج، وليس على المستودع كله في كل Commit.

ماذا وجدت كل أداة؟

الأداةما وجدتهما لا تستطيع أن تراه
أداة Brunoرمز الحالة 200 لطلب خطأالصفحة ومنطق الدالة الداخلي
أداة Cucumberنجحت القواعد، ثم كشفت قاعدة جديدة بلا كودأي قاعدة لم يكتبها أحد في ملف
أداة axeضعف التباين وقائمة بلا اسمحقل له placeholder بلا label، وكل ما يحتاج حكم إنسان
أداة Strykerاختبارات لا تلمس الحدود ولا رسالة الخطأال API والصفحة، فهو يقيس الاختبارات وليس التطبيق

وفي النهاية نحذف ما أنشأناه فقط: docker compose down يحذف lab-qa-app والشبكة، والأدوات الأخرى عملت بالخيار --rm، ثم نحذف مجلدات node_modules.

كيف تختار؟ جدول قرار حسب نوع فريقك

نوع الفريقابدأ بهذاأضف لاحقاً عند الحاجة
مطور واحد أو فريق صغير بلا مختبرإطار الوحدة للغتك، وBruno، وPlaywright لمسارين مهمين، وكلها في CIفحص axe داخل اختبارات Playwright نفسها
فريق فيه مختبرون يدويونما سبق، مع Kiwi TCMS على سيرفرك للحالات اليدوية والجولاتأداة Cucumber إذا كان مالك المنتج سيقرأ الأمثلة فعلاً
شركة كبيرة أو قطاع منظم يطلب أدلةأداة إدارة تحفظ التاريخ وتربط الحالات بالمتطلبات، مثل TestRail أو Kiwi TCMS باشتراك دعمتقارير سهولة الوصول Accessibility، واختبار الطفرات Mutation Testing على الوحدات الحساسة
خدمات كثيرة تملكها فرق مختلفةاختبارات API في CI لكل خدمة، وPact مع Pact Broker على سيرفركأداة k6 لاختبار الأحمال على المسارات المشتركة
تطبيق جوالأداة Maestro لأنها أسهل بداية، أو أطر المنصة إذا كان التطبيق أصلياً لمنصة واحدةأداة Appium عندما تحتاج لغة برمجة كاملة أو تكاملاً مع Selenium Grid
مشروع مفتوح المصدرأدوات مفتوحة فقط تعمل في CI عام: إطار الوحدة وPlaywright وBrunoالخطط المجانية من Percy أو Chromatic للصور

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

أخطاء شائعة

  • شراء أداة إدارة قبل وجود حالات تديرها: ابدأ بالحالات والأتمتة، وأضف الإدارة عندما يصبح السؤال «ماذا اختبرنا؟» صعب الإجابة.
  • مقارنة أدوات لا تتنافس: أداة Postman لا تغني عن Playwright، وأداة TestRail لا تختبر شيئاً.
  • الثقة بنتيجة أداة بعد تحديث مكتبة: كما رأيت مع Stryker وVitest 5، فإذا تغيرت النتيجة فجأة بعد تحديث فتحقق من توافق الإصدارات قبل أن تصدق الرقم.
  • اعتبار نجاح axe دليلاً على سهولة الوصول Accessibility: هو يجد جزءاً من المشاكل، والحقل بلا label مثال على ما يفوته.
  • ملفات Gherkin لا يقرؤها أحد: إذا كان المطور وحده يكتب ويقرأ ملفات Cucumber فاكتب اختبارات عادية واختصر الطبقة.
  • الأسعار من مقالات قديمة: خطط Postman تغيرت، وأدوات المرئيات المفتوحة توقفت، فراجع صفحة الشركة يوم القرار.
  • إصدارات غير مثبتة: ثبت إصدار كل أداة في CI كما فعلنا في التجربة، حتى لا يتغير السلوك دون أن يتغير الكود.

أين تكمل بعد هذه الخريطة؟

لكل فئة في الخريطة مقال عملي في مرحلة ضمان الجودة QA يشرح أداة واحدة منها بالتفصيل مع تجربة حقيقية:

الخلاصة

  • الأدوات عائلات تختبر طبقات مختلفة، والفريق الصغير يحتاج ثلاثاً أو أربعاً منها فقط: الوحدة وال API والمتصفح وCI.
  • أغلب أدوات الاختبار نفسها مفتوحة المصدر ومجانية، والتكلفة في الخدمات السحابية حولها: Cypress Cloud وPostman Team وGrafana Cloud k6 وPercy وPactFlow، ثم في أدوات الإدارة التي تحسب لكل مستخدم.
  • أداة Bruno تحفظ طلبات ال API كملفات في مستودعك، وPostman في سحابته، وخطط Postman القديمة لم تعد متاحة للعملاء الجدد.
  • أداة Kiwi TCMS هي الخيار المفتوح النشط لإدارة الحالات على سيرفرك، وTestLink شبه متوقف، وQase وTestRail سحابيان بسعر لكل مستخدم.
  • في التجربة وجد Bruno رمز حالة خاطئاً في ال API، ووجد axe مشكلتين في الصفحة وفاته الحقل بلا label، وكشف Cucumber قاعدة بلا كود، ووجد Stryker أن الاختبارات لا تلمس الحدود فارتفعت الدرجة من 87.80% إلى 100% بعد إضافة القيم الحدية Boundary Values.
  • تحقق من توافق الإصدارات قبل أن تصدق رقماً غريباً، فقد أعطى Stryker 12.20% خاطئة مع Vitest 5.0.4.

وفي مقالات قادمة من هذا القسم بإذن الله سوف نتعمق في كل عائلة، من إدارة حالات الاختبار Test Management على سيرفرك إلى اختبار المتصفح وال API والعقود والطفرات وسهولة الوصول Accessibility والجوال. وهذه مقالات مرتبطة بالموضوع:

دليلما هو ضمان الجودة QA وكيف يعمل فريق الاختبار؟كيف تفرق بين QA وQC والاختبار، وتصمم حالات الاختبار Test Cases التي تجد الأخطاء قبل أن يجدها عملاؤك.دليلأنواع الاختبارات للمبتدئين: من Unit إلى E2E مع Playwrightما الذي يكشفه كل نوع من الاختبارات وما الذي يفوته، وكم اختباراً تكتب من كل نوع، وأين يعمل كل منها في خط CI.دليلاختبارات يكتبها الذكاء الاصطناعي: متى تثق بها؟أين يساعدك الذكاء الاصطناعي في كتابة الاختبارات وأين يخدعك، ولماذا يبقى قرار ما هو الصحيح بيد الإنسان.دليلاختبار الأحمال والضغط Load Testing للمبتدئينلماذا يعمل تطبيقك مع خمسة مستخدمين وينهار مع خمسمئة، وكيف تكتشف ذلك وتصلحه قبل يوم الإطلاق.دليلDAST: فحص تطبيقك من الخارج وهو يعملكيف يفحص ZAP تطبيقك من الخارج وهو يعمل، ومتى يحق لك أن تشغله، وكيف تحول نتائجه إلى بوابة في خط CI.دليلSonarQube عملياً: بوابة جودة لكود الذكاء الاصطناعيكيف تثبت SonarQube مع PostgreSQL وتضبط قواعده وبوابته، ثم تجعل الوكيل الذكي يقرأ نتيجتها ويصلح كوده حتى تنجح.

سجل التحديثات

  • أكتوبر 2026: كتابة المقال اعتماداً على مستودعات الأدوات وصفحات الأسعار الرسمية كما هي في 12 أكتوبر 2026، مع تجربة حقيقية على Node.js 24.21.0 باستخدام Bruno CLI 4.2.1 وCucumber 13.3.0 وPlaywright 1.64.0 مع @axe-core/playwright 4.13.0 وaxe-core 4.13.0 وStryker 10.0.0 مع Vitest 4.1.11.
  • أكتوبر 2026: إضافة روابط المقالات العملية لكل فئة من أدوات الاختبار.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.