في مقال ضمان الجودة 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، وأداة متصفح لمسارين أو ثلاثة مهمة، و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/
├── 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-netdocker 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 يشرح أداة واحدة منها بالتفصيل مع تجربة حقيقية:
- إدارة حالات الاختبار Test Management مع Kiwi TCMS
- اختبارات الواجهة E2E مع Playwright
- اختبار ال API مع Bruno وPostman
- اختبار العقود Contract Testing مع Pact
- اختبار الطفرات Mutation Testing
- اختبار سهولة الوصول Accessibility لموقع عربي
- اختبار تطبيقات الجوال مع Appium وMaestro
الخلاصة
- الأدوات عائلات تختبر طبقات مختلفة، والفريق الصغير يحتاج ثلاثاً أو أربعاً منها فقط: الوحدة وال 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: إضافة روابط المقالات العملية لكل فئة من أدوات الاختبار.