كيف تبدأ اختبار الوحدات (Unit Testing) في كود JavaScript الخاص بك: دليل شامل للمطورين

دقائق القراءة: 10

يُجمع المطورون على أهمية كتابة اختبارات الوحدات (Unit Tests) لضمان جودة الكود، إلا أن التحدي غالبًا ما يكمن في معرفة نقطة البداية وكيفية تخصيص الوقت المناسب للاختبارات مقارنةً بعملية التطوير الفعلية. فهل يقتصر دور اختبارات الوحدات على التحقق من صحة الكود فحسب، أم أن لها فوائد أعمق؟

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

الأنواع المختلفة للاختبارات البرمجية

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

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

تهدف اختبارات الوحدات (Unit Tests) إلى فحص جزء واحد ومحدد من تطبيقك – أي “وحدة” واحدة. تتميز هذه الاختبارات بأنها مستقلة تمامًا؛ لا تتضمن أي تبعيات خارجية، أو عمليات دمج مع أنظمة أخرى، ولا ترتبط بتفاصيل إطار عمل معين. يمكن تشبيهها بدالة بسيطة تُرجع رابطًا بلغة محددة، كما في المثال التالي:

export function getAboutUsLink ( language ) {
  switch (language.toLowerCase()){
    case englishCode.toLowerCase():
      return '/about-us' ;
    case spanishCode.toLowerCase():
      return '/acerca-de' ;
  }
  return '' ;
}

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

في مرحلة ما من تطوير التطبيق، يتفاعل كودك مع قواعد البيانات، أو أنظمة الملفات، أو خدمات خارجية أخرى. قد يشمل ذلك أيضًا وحدات (modules) أخرى داخل تطبيقك. هذه الأجزاء من التنفيذ يجب أن تُختبر بواسطة اختبارات التكامل (Integration Tests). عادةً ما تتطلب هذه الاختبارات إعدادًا أكثر تعقيدًا يشمل تهيئة بيئات الاختبار، وتجهيز التبعيات (dependencies)، وغيرها من التحضيرات اللازمة.

اختبارات الوظائف (Functional Tests)

بينما تمنحك اختبارات الوحدات (Unit Tests) واختبارات التكامل (Integration Tests) الثقة في أن تطبيقك يعمل على المستوى التقني، فإن اختبارات الوظائف (Functional Tests) تنظر إلى التطبيق من منظور المستخدم النهائي. هدفها هو التأكد من أن النظام يعمل كما هو متوقع من وجهة نظر المستخدم، ويحقق الوظائف المطلوبة بالكامل.

مخطط يوضح هرمية الاختبارات البرمجية، حيث تشكل اختبارات الوحدات القاعدة الأكبر

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

لماذا يجب أن أهتم بكتابة اختبارات الوحدات (Unit Tests)؟

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

  • بناء الثقة في أن كودك يعمل بكفاءة: متى كانت آخر مرة قمت فيها بتغيير في الكود، ثم فشل البناء (build)، وتوقف نصف تطبيقك عن العمل؟ بالنسبة لي، كان ذلك الأسبوع الماضي. لكن هذا لا يزال مقبولًا. المشكلة الحقيقية تكمن عندما ينجح البناء، ويتم نشر التغيير، ثم يبدأ تطبيقك في أن يصبح غير مستقر. في هذه الحالة، تبدأ بفقدان الثقة في كودك، وفي النهاية، قد تكتفي بالدعاء ليعمل التطبيق. اختبارات الوحدات (Unit Tests) ستساعدك على اكتشاف المشكلات مبكرًا جدًا واستعادة ثقتك في جودة الكود.
  • اتخاذ قرارات معمارية (Architectural) أفضل: يتغير الكود باستمرار، لكن بعض القرارات الأساسية المتعلقة بالمنصة، الوحدات (modules)، الهيكلة، وغيرها، يجب اتخاذها في المراحل المبكرة من المشروع. عندما تبدأ التفكير في اختبار الوحدات (Unit Testing) منذ البداية، سيساعدك ذلك على هيكلة كودك بشكل أفضل وتحقيق فصل سليم للمسؤوليات (separation of concerns). لن تغريك فكرة إسناد مسؤوليات متعددة لكتل كود واحدة، لأن اختبارها كـ”وحدة” سيكون كابوسًا.
  • تحديد الوظائف بدقة قبل البدء بالبرمجة: غالبًا ما تبدأ بكتابة توقيع الدالة (method's signature) ثم تنتقل مباشرة إلى تطبيقها. ولكن، ماذا لو كان أحد المعاملات (parameters) فارغًا (null)؟ ماذا لو كانت قيمته خارج النطاق المتوقع أو تحتوي على عدد كبير جدًا من الأحرف؟ هل يجب أن تُطلق استثناءً (exception) أم تُرجع قيمة فارغة (null)؟ اختبارات الوحدات (Unit Tests) ستساعدك على اكتشاف كل هذه الحالات المحتملة. أعد النظر في هذه الأسئلة وستجد أنها بالضبط ما يحدد حالات اختبار وحدتك (unit test cases). أنا متأكد من وجود العديد من الفوائد الأخرى لكتابة اختبارات الوحدات، ولكن هذه هي أبرز ما أتذكره من تجربتي، وتلك التي تعلمتها بالطريقة الصعبة.

كيف تكتب أول اختبار وحدة (Unit Test) في JavaScript؟

لنعد الآن إلى عالم JavaScript. سنبدأ باستخدام Jest، وهو إطار عمل (framework) قوي ومشهور لاختبار JavaScript. يُعد Jest أداة تمكّن من إجراء اختبارات الوحدات (unit testing) تلقائيًا، وتوفر تقارير تغطية الكود (code coverage)، وتسمح لنا بمحاكاة الكائنات (mock objects) بسهولة. يتوفر لـ Jest أيضًا إضافة ممتازة لبرنامج Visual Studio Code يمكنك العثور عليها هنا. إذا كنت مهتمًا باستكشاف أطر عمل أخرى، يمكنك الاطلاع عليها في هذا المقال.

للبدء، قم بتثبيت Jest كمكتبة تطوير باستخدام الأمر التالي في سطر الأوامر (terminal):

npm i jest --save-dev

لنستخدم الدالة getAboutUsLink التي ذكرناها سابقًا كمثال للتطبيق الذي نرغب في اختباره:

const englishCode = "en-US" ;
const spanishCode = "es-ES" ;

function getAboutUsLink ( language ) {
  switch (language.toLowerCase()){
    case englishCode.toLowerCase():
      return '/about-us' ;
    case spanishCode.toLowerCase():
      return '/acerca-de' ;
  }
  return '' ;
}

module .exports = getAboutUsLink;

لقد وضعت هذا الكود في ملف باسم index.js. على الرغم من أنه يمكن كتابة الاختبارات في نفس الملف، إلا أن الممارسة الجيدة تقتضي فصل اختبارات الوحدات (unit tests) في ملف مخصص لها. تتضمن أنماط التسمية الشائعة لملفات الاختبار {filename}.test.js و {filename}.spec.js. في مثالنا هذا، سنستخدم النمط الأول، وسيكون اسم ملف الاختبار index.test.js:

const getAboutUsLink = require ( "./index" );

test( "Returns about-us for english language" , () => {
  expect(getAboutUsLink( "en-US" )).toBe( "/about-us" );
});

أولاً، نحتاج إلى استيراد الدالة التي نرغب في اختبارها. يتم تعريف كل اختبار كاستدعاء للدالة test. المعامل الأول هو اسم الاختبار لمرجعك، بينما المعامل الثاني هو دالة سهمية (arrow function) نقوم بداخلها باستدعاء الدالة المراد اختبارها وتحديد النتيجة المتوقعة. في هذه الحالة، نستدعي الدالة getAboutUsLink مع المعامل en-US للغة، ونتوقع أن تكون النتيجة /about-us.

الآن يمكننا تثبيت واجهة سطر الأوامر (CLI) الخاصة بـ Jest عالميًا وتشغيل الاختبار:

npm i jest-cli -g
jest

إذا واجهت خطأ يتعلق بالتكوين، تأكد من وجود ملف package.json في مشروعك. وإن لم يكن موجودًا، يمكنك إنشاؤه باستخدام الأمر npm init.

يجب أن ترى مخرجات مشابهة لما يلي:

PASS ./index.test.js
  √ Returns about-us for english language ( 4 ms)

  console .log index.js: 15
    /about-us

Test Suites: 1 passed, 1 total
Tests : 1 passed, 1 total
Snapshots : 0 total
Time : 2.389 s

عمل رائع! لقد أكملت للتو أول اختبار وحدة بسيط في JavaScript من البداية إلى النهاية. إذا كنت قد قمت بتثبيت إضافة Jest لبرنامج Visual Studio Code، فستقوم الإضافة بتشغيل الاختبارات تلقائيًا بمجرد حفظ الملف. دعنا نجرب ذلك عن طريق توسيع الاختبار بإضافة هذا السطر:

expect(getAboutUsLink( "cs-CZ" )).toBe( "/o-nas" );

بمجرد حفظ الملف، سيخبرك Jest بأن الاختبار قد فشل. تساعدك هذه الميزة على اكتشاف المشكلات المحتملة حتى قبل الالتزام بتغييراتك (committing your changes).

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

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

import { englishCode, spanishCode } from './LanguageCodes'

يمكنك استيراد هذه الثوابت إلى الاختبار بنفس الطريقة. لكن الوضع يصبح أكثر تعقيدًا إذا كنت تتعامل مع كائنات (objects) بدلاً من الثوابت البسيطة. لنلقِ نظرة على هذه الدالة:

import { UserStore } from './UserStore'

function getUserDisplayName ( ) {
  const user = UserStore.getUser(userId);
  return ` ${user.LastName} , ${user.FirstName} ` ;
}

تستخدم هذه الدالة الكائن المستورد UserStore:

class User {
  getUser(userId){
    // logic to get data from a database
  }

  setUser(user){
    // logic to store data in a database
  }
}

let UserStore = new User();
export { UserStore }

لاختبار هذه الدالة كوحدة (unit test) بشكل صحيح، نحتاج إلى محاكاة (mock) الكائن UserStore. المحاكاة هي بديل للكائن الأصلي، تسمح لنا بفصل التبعيات (dependencies) والبيانات الحقيقية عن تطبيق الدالة التي يتم اختبارها. يشبه الأمر استخدام الدمى في اختبارات تصادم السيارات بدلاً من أشخاص حقيقيين. إذا لم نستخدم المحاكاة، فسنكون نختبر كلاً من هذه الدالة و”المخزن” (store) معًا، وهذا سيتحول إلى اختبار تكامل (integration test) وسنحتاج على الأرجح إلى محاكاة قاعدة البيانات المستخدمة.

محاكاة خدمة (Mocking a Service)

لمحاكاة الكائنات (objects)، يمكنك إما توفير دالة محاكاة (mocking function) أو محاكاة يدوية (manual mock). سأركز على الخيار الأخير نظرًا لبساطة حالة الاستخدام هنا. ولكن لا تتردد في استكشاف إمكانيات المحاكاة الأخرى التي يوفرها Jest.

jest.mock( './UserStore' , () => ({
  UserStore : ({
    getUser : jest.fn().mockImplementation( arg => ({
      FirstName : 'Ondrej' ,
      LastName : 'Polesny'
    })),
    setUser : jest.fn()
  })
}));

أولاً، نحتاج إلى تحديد ما نقوم بمحاكاته – وهو الوحدة ./UserStore. بعد ذلك، يجب أن نُرجع الكائن المحاكي (mock) الذي يحتوي على جميع الكائنات المصدرة من تلك الوحدة. في هذا المثال، الكائن الوحيد هو كائن User المسمى UserStore مع الدالة getUser. لكن في التطبيقات الحقيقية، قد يكون الكائن المحاكي أطول بكثير. أي دوال لا تهتم بها حقًا في نطاق اختبار الوحدات (unit testing) يمكن محاكاتها بسهولة باستخدام jest.fn().

اختبار الوحدة للدالة getUserDisplayName يشبه الاختبار الذي أنشأناه سابقًا:

test( "Returns display name" , () => {
  expect(getUserDisplayName( 1 )).toBe( "Polesny, Ondrej" );
})

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

تقرير تغطية الكود (Code Coverage Report)

بعد أن تعلمنا كيفية اختبار كود JavaScript، من الجيد أن نغطي أكبر قدر ممكن من الكود بالاختبارات. وهذا أمر صعب بطبيعة الحال؛ فنحن بشر في النهاية، ونسعى لإنجاز مهامنا، وغالبًا ما تُعتبر اختبارات الوحدات (unit tests) عبئًا إضافيًا نميل إلى إهماله. هنا يأتي دور تغطية الكود (Code Coverage) كأداة قيمة تساعدنا في التغلب على هذا التحدي.

تُظهر لك تغطية الكود (Code Coverage) النسبة المئوية من كودك التي تغطيها اختبارات الوحدات. لنأخذ على سبيل المثال اختبار الوحدة الأول الذي قمنا به للدالة getAboutUsLink:

test( "Returns about-us for english language" , () => {
  expect(getAboutUsLink( "en-US" )).toBe( "/about-us" );
});

هذا الاختبار يتحقق من الرابط باللغة الإنجليزية، لكن النسخة الإسبانية تظل غير مختبرة. لذا، فإن تغطية الكود هنا هي 50%. أما اختبار الوحدة الآخر الذي يتحقق من الدالة getDisplayName فقد تم اختباره بالكامل وتغطية الكود له 100%. وبذلك، تكون تغطية الكود الإجمالية 67%. كان لدينا ثلاث حالات استخدام (use cases) للاختبار، لكن اختباراتنا غطت اثنتين منها فقط.

لرؤية تقرير تغطية الكود، اكتب الأمر التالي في سطر الأوامر (terminal):

jest --coverage

أو، إذا كنت تستخدم Visual Studio Code مع إضافة Jest، يمكنك تشغيل الأمر (CTRL+SHIFT+P) Jest: Toggle Coverage Overlay. سيعرض لك مباشرة في تطبيقك أي سطور من الكود غير مغطاة بالاختبارات.

لقطة شاشة لـ Visual Studio Code تعرض تراكب تغطية الكود، مع تمييز الأجزاء غير المغطاة

عند تشغيل فحص التغطية، سيقوم Jest أيضًا بإنشاء تقرير HTML مفصل. يمكنك العثور عليه في مجلد مشروعك ضمن المسار coverage/lcov-report/index.html.

تقرير HTML لتغطية الكود يظهر النسب المئوية للتغطية لكل ملف

لا داعي للقول إنك يجب أن تسعى جاهدًا لتحقيق تغطية كود بنسبة 100%، أليس كذلك؟ 🙂

الخلاصة

في هذا المقال، استعرضنا كيفية البدء باختبار الوحدات (unit testing) في JavaScript. وعلى الرغم من أن رؤية تقرير تغطية الكود بنسبة 100% أمر مرغوب فيه، إلا أنه في الواقع ليس من الممكن دائمًا تحقيق ذلك (بشكل هادف). الهدف الأساسي لاختبارات الوحدات هو مساعدتك في صيانة كودك وضمان عمله دائمًا كما هو متوقع. إنها تمكنك من: تحديد متطلبات التنفيذ بوضوح، تصميم كودك بشكل أفضل وفصل المسؤوليات، اكتشاف المشكلات التي قد تُدخلها مع التزاماتك الجديدة (newer commits)، وتمنحك الثقة في أن كودك يعمل بشكل صحيح. أفضل مكان للبدء هو صفحة “البدء” (Getting started) في توثيق Jest لتتمكن من تجربة هذه الممارسات بنفسك.

الخلاصة التقنية

تُعد اختبارات الوحدات (Unit Tests) حجر الزاوية في بناء تطبيقات JavaScript قوية ومستقرة. إنها ليست مجرد مهمة إضافية، بل استثمار يعود بالنفع على المدى الطويل من خلال تقليل الأخطاء، وتحسين جودة التصميم، وزيادة ثقة المطورين في الكود الذي يكتبونه. استخدام أدوات مثل Jest يجعل عملية الاختبار ميسرة وفعالة، مما يدعم دورة حياة تطوير برمجيات أكثر كفاءة وموثوقية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *