الدليل الشامل لاختبار تطبيقات JavaScript باستخدام Jest وCypress: من الصفر إلى الاحتراف

شهدت لغة جافا سكريبت (JavaScript) تطوراً هائلاً خلال العقد الأخير، حيث تحولت من مجرد لغة بسيطة لإضافة بعض الحركات التفاعلية على صفحات الويب إلى العمود الفقري لتطبيقات الويب الحديثة، سواء على مستوى الواجهات الأمامية (Frontend) باستخدام أطر عمل مثل React وVue وAngular، أو على مستوى الخلفية (Backend) باستخدام بيئة Node.js. ومع هذا التعقيد المتزايد في بنية التطبيقات، أصبح ضمان جودة الكود واستقراره أمراً بالغ الأهمية. هنا تبرز أهمية اختبار البرمجيات (Software Testing) كركيزة أساسية في دورة حياة تطوير البرمجيات الحديثة.

كتابة الأكواد البرمجية دون اختبارها تشبه بناء ناطحة سحاب دون فحص أساساتها؛ قد تبدو صلبة في البداية، لكنها سرعان ما تنهار عند مواجهة أي ضغط أو تحديثات مستقبلية. يساعدنا اختبار الكود في اكتشاف الأخطاء (Bugs) مبكراً قبل وصول التطبيق إلى المستخدم النهائي، ويسهل عملية إعادة هيكلة الكود (Refactoring) بثقة، ويضمن أن الميزات الجديدة لا تؤثر سلباً على الوظائف القائمة بالفعل. في هذا الدليل الشامل، سنتعمق في كيفية اختبار كود JavaScript باستخدام أداتين من أقوى وأشهر الأدوات الحديثة في هذا المجال: Jest المخصصة لاختبار الوحدات والمكونات، وCypress الرائدة في اختبار النهاية إلى النهاية (End-to-End Testing).

فهم هرم الاختبار (The Testing Pyramid) وأهميته

قبل الدخول في التفاصيل التقنية لكتابة الاختبارات، من الضروري فهم الفلسفة الكامنة وراء اختبار البرمجيات، والتي تلخصها عادةً فكرة “هرم الاختبار”. يقسم هذا الهرم الاختبارات إلى ثلاث طبقات رئيسية:

1. اختبارات الوحدات (Unit Tests)

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

2. اختبارات التكامل (Integration Tests)

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

3. اختبارات النهاية إلى النهاية (End-to-End Tests)

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

الجزء الأول: اختبار الوحدات باستخدام Jest

تُعد أداة Jest، التي طورتها شركة Facebook (Meta حالياً)، إطار العمل الأكثر شعبية لاختبار كود JavaScript. تتميز Jest بأنها تأتي محملة بكل ما تحتاجه للبدء فوراً دون الحاجة إلى تكوينات معقدة (Zero Configuration)، بالإضافة إلى سرعتها العالية بفضل تشغيل الاختبارات بشكل متوازٍ، ودعمها المدمج للمحاكاة (Mocking) وقياس تغطية الكود (Code Coverage).

خطوات إعداد وتثبيت Jest في مشروعك

للبدء في استخدام Jest، سنقوم بتهيئة مشروع Node.js جديد وتثبيت الحزمة كاعتماد تطويري (Development Dependency). افتح واجهة السطر البرمجي (Terminal) في مجلد مشروعك واكتب الأوامر التالية:

npm init -y
npm install --save-dev jest

بعد اكتمال التثبيت، افتح ملف package.json وقم بتعديل قسم السكربتات (scripts) ليتضمن أمر تشغيل Jest بسهولة:

{
  "scripts": {
    "test": "jest"
  }
}

كتابة أول اختبار بسيط باستخدام أدوات المطابقة (Matchers)

لنقم بإنشاء دالة بسيطة تقوم بعملية حسابية لنرى كيف يمكننا اختبارها. سننشئ ملفاً باسم mathOperations.js ونكتب فيه الكود التالي:

function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

module.exports = { add, multiply };

الآن، سنقوم بإنشاء ملف الاختبار الخاص بهذه الدوال. تبحث Jest تلقائياً عن الملفات التي تنتهي بـ .test.js أو .spec.js. سننشئ ملفاً باسم mathOperations.test.js:

const { add, multiply } = require('./mathOperations');

describe('اختبار الدوال الحسابية الأساسية', () => {
  test('يجب أن تقوم دالة الجمع بجمع رقمين بشكل صحيح', () => {
    expect(add(2, 3)).toBe(5);
  });

  test('يجب أن تقوم دالة الضرب بضرب رقمين بشكل صحيح', () => {
    expect(multiply(4, 3)).toBe(12);
  });
});

في هذا الكود، استخدمنا الدالة describe لتجميع الاختبارات ذات الصلة معاً، وهي ممارسة ممتازة لتنظيم ملفات الاختبار وتسهيل قراءة التقارير. الدالة test (أو بديلتها it) تُستخدم لتعريف حالة اختبار فردية. أما الدالة expect فهي نقطة الانطلاق للتحقق من النتائج، وتأتي متبوعة بأداة مطابقة مثل toBe للتحقق من القيم البسيطة.

لتشغيل الاختبار، اكتب الأمر التالي في Terminal:

npm test

ستظهر لك نتيجة الاختبار في السطر البرمجي باللون الأخضر لتؤكد نجاح الاختبارات.

أدوات مطابقة متقدمة في Jest

لا تقتصر Jest على toBe فقط؛ بل توفر مجموعة واسعة من أدوات المطابقة لتغطية مختلف الحالات:

  • toEqual: تُستخدم لمقارنة الكائنات (Objects) والمصفوفات (Arrays) بشكل عميق للتأكد من تطابق المحتوى وليس المرجع في الذاكرة فقط.
  • toBeNull, toBeUndefined, toBeDefined: للتحقق من حالة المتغيرات وقيمها الخاصة.
  • toContain: للتحقق مما إذا كانت مصفوفة أو نص يحتوي على عنصر أو كلمة معينة.
  • toThrow: للتحقق مما إذا كانت دالة معينة تقوم برمي خطأ (Error) عند استدعائها بمدخلات خاطئة.

اختبار الكود غير المتزامن (Asynchronous Code) في Jest

في تطبيقات الويب الحقيقية، نتعامل باستمرار مع العمليات غير المتزامنة مثل جلب البيانات من واجهات برمجة التطبيقات (APIs) أو قراءة الملفات. توفر Jest دعماً ممتازاً لاختبار هذه العمليات باستخدام الوعود (Promises) أو صيغة async/await الحديثة.

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

// userService.js
const fetch = require('node-fetch');

async function getUserData(userId) {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
  if (!response.ok) {
    throw new Error('فشل جلب البيانات');
  }
  return await response.json();
}

module.exports = { getUserData };

لكتابة اختبار لهذه الدالة غير المتزامنة، يمكننا استخدام async/await داخل دالة الاختبار نفسها كالتالي:

// userService.test.js
const { getUserData } = require('./userService');

test('يجب أن تجلب بيانات المستخدم بشكل صحيح بناءً على المعرف', async () => {
  const data = await getUserData(1);
  expect(data).toHaveProperty('id');
  expect(data.id).toBe(1);
  expect(data).toHaveProperty('name');
});

باستخدام async قبل تعريف دالة الاختبار وawait قبل استدعاء الدالة المراد اختبارها، نضمن أن Jest ستنتظر حتى تكتمل العملية غير المتزامنة بالكامل قبل تقييم النتيجة وإصدار حكمها على نجاح الاختبار أو فشله.

قوة المحاكاة (Mocking) في اختبارات الوحدات

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

هنا يأتي دور المحاكاة (Mocking). تتيح لنا Jest استبدال الدوال الحقيقية أو الوحدات الخارجية بنسخ وهمية (Mocks) يمكننا التحكم في سلوكها وتحديد القيم التي ترجعها بدقة.

لنقم بمحاكاة دالة جلب البيانات لتجنب إجراء طلب شبكة حقيقي:

// mockExample.test.js
const { getUserData } = require('./userService');

// نقوم بمحاكاة مكتبة node-fetch بالكامل
jest.mock('node-fetch');
const fetch = require('node-fetch');

test('يجب اختبار جلب البيانات باستخدام المحاكاة دون الاتصال بالإنترنت', async () => {
  // نحدد السلوك الوهمي لدالة fetch
  fetch.mockResolvedValue({
    ok: true,
    json: jest.fn().mockResolvedValue({ id: 1, name: 'أحمد علي' })
  });

  const data = await getUserData(1);
  expect(data.name).toBe('أحمد علي');
  // نتأكد من أن دالة fetch تم استدعاؤها بالفعل بالرابط الصحيح
  expect(fetch).toHaveBeenCalledWith('https://jsonplaceholder.typicode.com/users/1');
});

من خلال jest.mock، قمنا بقطع الاتصال بالخارج واستبداله ببيانات محلية فورية. هذا يجعل اختباراتنا سريعة البرق وتعمل بكفاءة حتى في بيئات التطوير التي تفتقر إلى اتصال بالإنترنت.

ميزات Jest المتقدمة: قياس تغطية الكود (Code Coverage)

كيف تعرف ما إذا كنت قد كتبت اختبارات كافية لتغطية كافة أجزاء تطبيقك؟ توفر Jest أداة مدمجة رائعة لقياس “تغطية الكود”. تقوم هذه الأداة بتحليل الكود الخاص بك وتحديد الأسطر، والمسارات الشرطية (مثل أجزاء if/else)، والدوال التي تم تنفيذها أثناء الاختبار وتلك التي تم تجاهلها.

لتشغيل تقرير التغطية، ببساطة أضف المعامل --coverage إلى أمر التشغيل:

npm test -- --coverage

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

الجزء الثاني: اختبار النهاية إلى النهاية (E2E) باستخدام Cypress

بينما تركز Jest على التفاصيل الدقيقة للأكواد البرمجية بشكل منعزل، تأتي أداة Cypress لتختبر التطبيق ككل متكامل. Cypress هي أداة حديثة وقوية مصممة خصيصاً لاختبار كل ما يعمل داخل المتصفح. على عكس الأدوات القديمة مثل Selenium التي تعتمد على برامج تشغيل خارجية وتتسم بالبطء وصعوبة الإعداد، تعمل Cypress مباشرة داخل نفس حلقة الحدث (Event Loop) الخاصة بالمتصفح، مما يمنحها سرعة فائقة وقدرة على التحكم الكامل في التطبيق.

أبرز ميزات Cypress الثورية

  • السفر عبر الزمن (Time Travel): تلتقط Cypress لقطات شاشة (Snapshots) للتطبيق عند كل خطوة من خطوات الاختبار. يمكنك تصفح هذه الخطوات في لوحة التحكم التفاعلية ورؤية كيف كان يبدو التطبيق بالضبط عند كل نقرة أو إدخال.
  • التصحيح الفوري (Debuggability): يمكنك استخدام أدوات المطور المعتادة في المتصفح (Chrome DevTools) مباشرة أثناء تشغيل الاختبارات لفحص الأخطاء ووضع نقاط التوقف (Breakpoints).
  • الانتظار التلقائي (Automatic Waiting): لا حاجة لكتابة أكواد انتظار غير موثوقة مثل sleep أو wait؛ تنتظر Cypress تلقائياً ظهور العناصر على الشاشة قبل التفاعل معها.
  • تسجيل الفيديو ولقطات الشاشة: تقوم Cypress تلقائياً بتسجيل فيديو للاختبار بأكمله والتقاط صور عند حدوث أي فشل، وهو أمر لا يثمن بثمن عند تشغيل الاختبارات في بيئات التطوير المستمر (CI/CD).

إعداد وتثبيت Cypress

لتثبيت Cypress في مشروعك، قم بتشغيل الأمر التالي في مجلد المشروع:

npm install --save-dev cypress

بمجرد اكتمال التثبيت، يمكنك فتح واجهة Cypress الرسومية التفاعلية لأول مرة باستخدام الأمر:

npx cypress open

سيؤدي هذا الأمر إلى فتح نافذة ترحيبية تساعدك في إعداد بيئة الاختبار واختيار نوع الاختبار (E2E أو Component Testing)، وتقوم تلقائياً بإنشاء الهيكل التنظيمي للمجلدات وملف الإعداد الرئيسي cypress.config.js.

كتابة أول اختبار تفاعلي باستخدام Cypress

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

داخل المجلد الذي أنشأته Cypress تلقائياً (عادةً cypress/e2e)، سننشئ ملفاً باسم login.spec.js ونكتب فيه السيناريو التالي:

describe('اختبار نظام تسجيل الدخول', () => {
  it('يجب أن يعرض رسالة خطأ عند إدخال بيانات غير صحيحة', () => {
    // 1. زيارة صفحة تسجيل الدخول
    cy.visit('https://example.com/login');

    // 2. البحث عن حقل البريد الإلكتروني وإدخال القيمة
    cy.get('input[name="email"]')
      .type('wronguser@example.com')
      .should('have.value', 'wronguser@example.com');

    // 3. البحث عن حقل كلمة المرور وإدخال قيمة خاطئة
    cy.get('input[name="password"]')
      .type('wrongpassword');

    // 4. الضغط على زر تسجيل الدخول
    cy.get('button[type="submit"]').click();

    // 5. التحقق من ظهور رسالة الخطأ المناسبة للمستخدم
    cy.get('.error-message')
      .should('be.visible')
      .and('contain', 'البريد الإلكتروني أو كلمة المرور غير صحيحة');
  });
});

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

  • cy.visit(): لتوجيه المتصفح لزيارة رابط معين.
  • cy.get(): لاختيار العناصر من الصفحة باستخدام محددات CSS (CSS Selectors).
  • cy.type(): لمحاكاة الكتابة على لوحة المفاتيح داخل حقول الإدخال.
  • cy.click(): لمحاكاة النقر بالماوس.
  • cy.should(): لإجراء التأكيدات (Assertions) والتحقق من حالة العناصر وصحتها.

الدمج بين Jest وCypress في دورة حياة التطوير

السؤال الشائع الذي يطرحه الكثير من المطورين هو: “هل يجب أن أستخدم Jest أم Cypress؟” والإجابة الصحيحة هي: كلاهما معاً. فهما لا يتنافسان بل يكملان بعضهما البعض لتقديم تغطية اختبار شاملة وقوية للتطبيق.

يتجلى الدمج المثالي في استخدام Jest لاختبار المنطق الداخلي للتطبيق (Business Logic)، والدوال الحسابية، وعمليات معالجة البيانات، ومكونات الواجهة الأمامية بشكل مستقل وسريع. بينما نستخدم Cypress لاختبار السيناريوهات الحيوية والمسارات الرئيسية للمستخدم (Critical User Flows) مثل عملية الشراء الكاملة في متجر إلكتروني، أو خطوات إنشاء حساب جديد وتفعيله. هذا المزيج يضمن لك سرعة التطوير بفضل اختبارات الوحدات السريعة، وأمان التشغيل بفضل اختبارات E2E الشاملة.

أفضل الممارسات لكتابة اختبارات برمجية عالية الجودة

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

1. اكتب اختبارات مستقلة تماماً

يجب ألا يعتمد أي اختبار على نتيجة اختبار آخر. يجب أن يكون كل اختبار قادراً على العمل بمفرده وبأي ترتيب. في Cypress، تجنب الاعتماد على بقاء المستخدم مسجلاً للدخول من اختبار سابق، بل قم بتهيئة الحالة المناسبة قبل كل اختبار باستخدام الدوال المساعدة مثل beforeEach.

2. تجنب اختبار التفاصيل الداخلية (Implementation Details)

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

3. استخدم محددات مستقرة للاختبار

في اختبارات Cypress، قد تتغير فئات التنسيق (CSS Classes) باستمرار بسبب إعادة التصميم أو استخدام أدوات مثل Tailwind CSS. لتجنب كسر الاختبارات عند كل تغيير في التصميم، يُنصح باستخدام سمات مخصصة للاختبار مثل data-testid="submit-button" واستهدافها في اختباراتك بدلاً من الفئات العامة.

4. حافظ على سرعة الاختبارات

تذكر دائماً هرم الاختبار؛ يجب أن تكون اختبارات الوحدات السريعة هي الغالبية العظمى في مشروعك، بينما تقتصر اختبارات E2E البطيئة على المسارات الأكثر أهمية وحساسية لتقليل الوقت المستغرق في عمليات البناء والتشغيل التلقائي.

إن تبني ثقافة اختبار الكود ليس مجرد رفاهية تقنية، بل هو استثمار حقيقي طويل الأجل في جودة منتجك البرمجي وراحة بالك كمطور. عندما تمتلك شبكة أمان متكاملة من اختبارات الوحدات القوية باستخدام Jest واختبارات النهاية إلى النهاية التفاعلية باستخدام Cypress، ستكتسب الثقة الكاملة لإجراء أي تعديلات، وإضافة ميزات جديدة معقدة، وإطلاق تحديثاتك إلى المستخدمين في أي وقت من اليوم دون الخوف من حدوث مفاجآت غير متوقعة أو توقف الخدمات الحيوية، مما ينعكس بشكل مباشر على نجاح مشروعك ورضا عملائك واستقرار بيئة عملك البرمجية.

Frequently Asked Questions

ما الفرق الجوهري في بيئة التشغيل بين اختبارات Jest واختبارات Cypress؟

تُشغل Jest الاختبارات في بيئة افتراضية تعتمد على Node.js وjsdom لمحاكاة المتصفح، مما يجعلها سريعة جداً ومثالية لاختبارات الوحدات. أما Cypress، فتُشغل الاختبارات داخل متصفح حقيقي (مثل Chrome أو Firefox)، مما يتيح محاكاة دقيقة لتجربة المستخدم النهائي واختبار التفاعلات المعقدة.

هل يجب عليّ كتابة اختبارات Cypress لكل ميزة في تطبيقي لضمان جودته؟

لا، وفقاً لهرم الاختبار، يجب أن تكون اختبارات Cypress (End-to-End) هي الأقل عدداً لأنها تستهلك وقتاً وموارد كبيرة. يُنصح بالتركيز على مسارات المستخدم الحساسة مثل تسجيل الدخول وإتمام الدفع، وترك التفاصيل الصغيرة لاختبارات الوحدات باستخدام Jest.

لماذا يُفضل تثبيت Jest كاعتماد تطويري (devDependencies) وليس كاعتماد عادي؟

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

كيف تتعامل Jest مع اختبار الدوال التي تعتمد على بيانات خارجية دون الاتصال بالإنترنت؟

توفر Jest ميزة المحاكاة المدمجة (Mocking)، والتي تتيح لك استبدال الدوال التي تجلب البيانات من خوادم خارجية بنسخ وهمية تُرجع بيانات محددة مسبقاً. هذا يضمن بقاء الاختبارات سريعة، ومستقلة، وغير معتمدة على استقرار شبكة الإنترنت.


اكتشاف المزيد من كود التطور

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك رد

Scroll to Top