اذهب إلى المحتوى

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

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

سنكتب في هذا المقال اختبارات لتطبيق قائمة مهام سنطوره ضمن بيئة نود، وسنُعد إطار عمل الاختبارات موكا Mocha له ونستخدمه لتنظيم الاختبارات، ثم سنستخدم الوحدة assert لكتابة تلك الاختبارات، أي سنستخدم Mocha لتخطيط الاختبارات والوحدة assert لتنفيذ التوكيدات ضمن الاختبار، وسيلزمك لتطبيق الأمثلة في هذا المقال تثبيت بيئة Node.js على جهازك، حيث سنستعمل في هذا المقال الإصدار رقم 10.16.0، وأيضًا معرفة بأساسيات لغة جافاسكربت.

كتابة الوحدة البرمجية في نود

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

والآن نبدأ بتحضير بيئة العمل وننشئ مجلد باسم المشروع todos:

mkdir todos

ثم ندخل إلى المجلد:

cd todos

ونهيّئ ملف حزمة npm لاستخدامه لاحقًا لتنفيذ أوامر الاختبار:

npm init -y

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

npm i request --save-dev mocha

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

touch index.js

ونفتحه ضمن أي محرر النصوص وليكن باستخدام محرر نانو nano:

nano index.js

نبدأ بتعريف الصنف Todos والذي سيحتوي على توابع سنستخدمها لإدارة قائمة المهام، لذا نضيف الأسطر التالية إلى ملف index.js:

class Todos {
    constructor() {
        this.todos = [];
    }
}

module.exports = Todos;

عرّفنا في بداية الملف الصنف Todos والتابع الباني له constructor()‎ بدون أي معاملات، حيث يمكننا إنشاء كائن جديد من هذا الصنف دون الحاجة لتمرير أي قيم له، ومهمته حاليًا إنشاء الخاصية todos وتعيين مصفوفة فارغة كقيمة لها، ثم صدّرنا هذا الصنف باستخدام الكائن modules في النهاية، كي تتمكن باقي الوحدات البرمجية من استيراد واستخدام الصنف Todos، فبدون ذلك لا يمكن لملف الاختبار الذي سنُنشئه لاحقًا استيراد واستخدام هذا الصنف، والآن نضيف تابعًا وظيفته إرجاع مصفوفة المهام المخزنة ضمن الكائن كالتالي:

class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }
}

module.exports = Todos;

يعيد التابع list()‎ نسخة من المصفوفة المخزنة ضمن الصنف باستخدام صيغة التفكيك في جافاسكربت لأن إعادة المتغير this.todos مباشرة يعني إعادة مؤشر إلى المصفوفة الأصلية ضمن الصنف Todos وبذلك نمنع الوصول إلى المصفوفة الأصلية وإجراء تعديلات عليها عن طريق الخطأ.

ملاحظة: المصفوفات في جافاسكربت تُمرَّر بالمرجعية reference (وكذلك الكائنات objects أيضًا)، أي عند إسناد مصفوفة إلى متغير فإنه يحمل إشارة إلى تلك المصفوفة الأصلية وليس المصفوفة نفسها أي عند استعمال هذا المتغير لاحقًا أو تمريره كمعامل لتابع ما، فستشير جافاسكربت إلى المصفوفة الأصلية دومًا وستنعكس التعديلات عليها، فمثلًا إذا عند إنشاء مصفوفة تحوي ثلاث عناصر أسندناها إلى متغير x، ثم أنشأنا المتغير y وأسندنا له قيمة المصفوفة السابقة كالتالي y = x، فسيشير عندها كل من y و x إلى نفس المصفوفة وكل تغيير نقوم به على المصفوفة عن طريق المتغير y سيؤثر على المصفوفة التي يشير إليها المتغير x والعكس صحيح أي كلاهما يشيران إلى المصفوفة نفسها.

والآن لنضيف التابع add()‎ ووظيفته إضافة مهمة جديدة إلى قائمة المهام الحالية:

class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }

        this.todos.push(todo);
    }
}

module.exports = Todos;

يأخذ التابع add()‎ معاملًا من نوع سلسلة نصية ويضعها ضمن خاصية العنوان title لكائن المهمة الجديدة، ويعين خاصية اكتمال هذه المهمة completed بالقيمة false افتراضيًا، ثم يضيف ذلك الكائن إلى مصفوفة المهام الحالية ضمن الكائن.

ومن المهام الأخرى التي يجب أن يوفرها صنف مدير المهام هو تعيين مهمة كمهمة مكتملة، حيث سننفذ ذلك بالمرور على عناصر مصفوفة المهام todos والبحث عن عنصر المهمة التي يريد المستخدم تعيينها كمهمة مكتملة، وعند العثور عليها نعينها كمكتملة وإذا لم يُعثر عليها نرمي خطأ كإجراء احترازي، والآن نضيف هذا التابع الجديد complete()‎ كالتالي:

class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }

        this.todos.push(todo);
    }

    complete(title) {
        let todoFound = false;
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }
}

module.exports = Todos;

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

اختبار الشيفرة يدويًا

في هذه الفقرة سننفذ شيفرات التوابع السابقة لصنف إدارة المهام Todos يدويًا لنعاين ونتفحص خرجها ونتأكد من عملها كما هو متوقع منها أن تعمل، وتدعى هذه الطريقة بالاختبار اليدوي manual testing فهي أشيع طريقة يطبقها المطورون معظم الوقت حتى لو لكن يكن ذلك مقصودًا، وسنؤتمت لاحقًا تلك العملية باستخدام موكا Mocha لكن بدايةً سنختبر الشيفرات يدويًا لنتعرف على هذه الطريقة ونلاحظ ميزة استخدام إطار خاص لأتمتة الاختبارات.

نبدأ بإضافة مهمتين جديدتين ونعيّن إحداهما كمكتملة، لذلك نبدأ جلسة REPL جديدة ضمن مجلد المشروع نفسه الحاوي على الملف index.js كالتالي:

node

ستلاحظ ظهور الرمز ‎>‎ في بداية السطر عند الدخول إلى وضع REPL التفاعلي، ويمكننا إدخال شيفرات جافاسكربت لتنفيذها كالتالي:

const Todos = require('./index');

نحمل الوحدة البرمجية لمدير قائمة المهام باستخدام التابع require()‎ ونخزن قيمتها ضمن متغير بالاسم Todos، والذي صدرنا منه افتراضيًا الصنف Todos، والآن لنبدأ بإنشاء كائن جديد من ذلك الصنف كالتالي:

const todos = new Todos();

يمكننا اختبار الوحدة باستخدام الكائن todos المشتق من الصنف Todos للتأكد من عمله وفق ما هو متوقع، فنبدأ بإضافة مهمة جديدة كالتالي:

todos.add("run code");

لم يظهر إلى الآن مما نفذناه أي خرج ضمن الطرفية، ولنتأكد من تخزين المهمة السابقة بشكل سليم ضمن قائمة المهام نستدعي تابع عرض المهام الموجودة ونعاين النتيجة:

todos.list();

سيظهر لنا الخرج التالي:

[ { title: 'run code', completed: false } ]

وهي النتيجة الصحيحة المتوقعة حيث تحتوي على عنصر وحيد وهو المصفوفة التي أضفناها سابقًا وحالة اكتمالها غير مكتملة، لنضيف الآن مهمة أخرى ونعدل المهمة الأولى لتصبح مكتملة كالتالي:

todos.add("test everything");
todos.complete("run code");

نتوقع الآن وجود مهمتين ضمن الكائن todos، وهما "run code" و "test everything"، حيث يجب أن تكون المهمة الأولى "run code" مكتملة، ونتأكد من ذلك باستدعاء التابع list()‎:

todos.list();

نحصل على الخرج:

[
  { title: 'run code', completed: true },
  { title: 'test everything', completed: false }
]

الخرج صحيح كما هو متوقع، والآن نخرج من جلسة REPL بتنفيذ الأمر التالي:

.exit

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

كتابة اختبارات Node.js باستخدام Mocha و Assert

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

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

touch index.test.js

نفتحه ضمن أي محرر نصوص:

nano index.test.js

نبدأ بتحميل الوحدة البرمجية لمدير المهام كما فعلنا ضمن جلسة REPL في الفقرة السابقة، وبعدها نحمل الوحدة البرمجية assert لاستخدامها عند كتابة الاختبارات كالتالي:

const Todos = require('./index');
const assert = require('assert').strict;

تسمح الخاصية strict التي استخرجناها من الوحدة assert باستخدام معامل مساواة خاص منصوح باستخدامه ضمن بيئة نود ويوفر مزايا مفيدة أخرى لن ندخل في تفاصيلها.

والآن قبل كتابة الاختبارات لنتعرف على طريقة موكا Mocha في تنظيم وترتيب شيفرات الاختبار، حيث تُكتب الاختبارات في Mocha بالصيغة التالية:

describe([Test Group Name], function() {
    it([Test Name], function() {
        [Test Code]
    });
});

لاحظ وجود استدعاء لدالتين رئيسيين هما describe()‎ و it()‎، حيث تستخدم الدالة describe()‎ لتجميع الاختبارات المتشابهة معًا المكتوبة عبر it()‎، ولكن خطوة التجميع هذه غير ضرورية لكنها تسهل قراءة ملفات الاختبار وتزيد تنظيمها ويسهل لاحقًا التعديل على الاختبارات المتشابهة بسهولة أكبر، أما الدالة it()‎ فتحتوي على شيفرة الاختبار المراد تنفيذها للوحدة البرمجية المختبرة ونستخدم فيها مكتبة assert للتوكيد والتحقق من المخرجات.

وهدفنا في هذه الفقرة استخدام موكا Mocha والوحدة assert لأتمتة عملية الاختبار أو حتى تنفيذها يدويًا كما فعلنا سابقًا، لذلك سنبدأ أولٌا بتعريف مجموعة اختبارات باستخدام التابع describe()‎ بإضافة الأسطر التالية لملف الاختبار بعد استيراد الوحدات البرمجية السابقة:

...
describe("integration test", function() {
});

بهذا نكون قد أنشأنا مجموعة اختبارات -سنكتبها لاحقًا- باسم integration test أي اختبار التكامل ووظيفته التحقق من عمل عدة توابع مع بعضها ضمن الوحدات البرمجية، على عكس اختبار الوحدة unit test الذي يختبر دالة واحدة في كل مرة، وعندما ينفذ موكا عملية اختبار التطبيق فسينفذ كل الاختبارات المعرفة ضمن التابع describe()‎ ضمن مجموعة بالاسم "integration test" التي عرفناها.

والآن لنضيف اختبارًا باستخدام التابع it()‎ لاختبار جزء من التطبيق:

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
    });
});

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

والآن نبدأ بأول اختبار وهو إنشاء كائن من الصنف Todos جديد والتأكد بأنه لا يحتوي على أي عناصر:

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.notStrictEqual(todos.list().length, 1);
    });
});

أنشأنا في أول سطر من الاختبار كائنًا جديدًا من الصنف Todos كما فعلنا سابقًا ضمن REPL أو كما سنفعل عند استخدام هذا الصنف ضمن أي وحدة برمجية أخرى، واستخدمنا في السطر الثاني الوحدة assert وتحديدًا تابع اختبار عدم المساواة notStrictEqual()‎ والذي يأخذ معاملان وهما القيمة التي نريد اختبارها وتدعى القيمة الفعلية actual، والمعامل الثاني وهو القيمة التي نتوقع أن لا تساويها وتدعى القيمة المتوقعة expected، وفي حال تساوي القيمتين سيرمي التابع notStrictEqual()‎ خطئًا ويفشل هذا الاختبار.

نحفظ الملف ونخرج منه، ونتوقع في هذه الحالة نجاح هذا الاختبار لأن طول المصفوفة سيكون 0 وهو غير مساوي للقيمة 1، ونتأكد من ذلك بتشغيل الاختبارات باستخدام موكا، لذا نعدل بدايةً على ملف الحزمة package.json ونفتحه ضمن محرر النصوص ونعدل النص البرمجي الخاص بتشغيل الاختبارات ضمن الخاصية scripts كالتالي:

...
"scripts": {
    "test": "mocha index.test.js"
},
...

بذلك نكون قد عدلنا الأمر test الخاص بالأداة npm، حيث عند تنفيذه كالتالي npm test سيتحقق npm من الأمر الذي أدخلناه ضمن ملف الحزمة package.json وسيبحث عن مكتبة موكا Mocha ضمن مجلد الحزم node_modules وينفذ الأمر mocha مُمرِّرًا له اسم ملف الاختبار للتطبيق.

والآن نحفظ الملف ونخرج منه وننفذ أمر الاختبار السابق ونعاين النتيجة كالتالي:

npm test

نحصل على الخرج:

> todos@1.0.0 test your_file_path/todos
> mocha index.test.js



integrated test
     should be able to add and complete TODOs


  1 passing (16ms)

يُظهر لنا الخرج السابق مجموعة الاختبارات التي جرى تنفيذها ويترك فراغًا قبل كل اختبار ضمن مجموعة الاختبار المعرفة، ونلاحظ ظهور اسم الاختبار كما مررناه للتابع it()‎ في ملف الاختبار، حيث تشير العلامة الظاهرة على يسار الاختبار أن الاختبار قد نجح، وفي الأسفل يظهر لنا خلاصة فيها معلومات عن كل الاختبارات التي نُفِّذت، وفي حالتنا هناك اختبار واحد ناجح واستغرقت عملية الاختبار كاملة 16 ميلي ثانية لتنفيذها، حيث يعتمد هذا التوقيت على أداء الجهاز الذي يُنفذ تلك الاختبارات.

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

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.notStrictEqual(todos.list().length, 1);
    });
});

نحفظ الملف ونخرج منه ونلاحظ أننا أضفنا مهمتين جديدتين، لننفذ الاختبار ونلاحظ النتيجة:

npm test

سيظهر لنا التالي:

...
integrated test
     should be able to add and complete TODOs


  1 passing (8ms)

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

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

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        todos.add("get up from bed");
        todos.add("make up bed");
        assert.strictEqual(todos.list().length, 0);
    });
});

لاحظ استدعينا التابع strictEqual()‎ بدلًا من استدعاء التابع notStrictEqual()‎ الذي يتحقق من المساواة بين القيمة الحقيقية والمتوقعة الممررة له، بحيث يفشل عند عدم تساوي القيمتين.

والآن نحفظ الملف ونخرج منه ونعيد تنفيذ أمر الاختبار لنرى النتيجة:

npm test

هذه المرة سيظهر لنا خطأ:

...
  integration test
    1) should be able to add and complete TODOs


  0 passing (16ms)
  1 failing

  1) integration test
       should be able to add and complete TODOs:

      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
+ expected - actual

- 2
+ 0
      + expected - actual

      -2
      +0

      at Context. (index.test.js:9:10)



npm ERR! Test failed.  See above for more details.

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

...
0 passing (29ms)
  1 failing
...

والخرج الباقي يظهر بيانات متعلقة بالاختبارات الفاشلة، حيث يظهر أولًا الاختبارات التي فشلت:

...
1) integrated test
       should be able to add and complete TODOs:
...

ثم سبب فشل تلك الاختبارات:

...
      AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
+ expected - actual

- 2
+ 0
      + expected - actual

      -2
      +0

      at Context. (index.test.js:9:10)
...

رُمي خطأ من النوع AssertionError عندما فشل اختبار التابع strictEqual()‎، حيث نلاحظ أن القيمة المتوقعة وهي 0 مختلفة عن القيمة الحقيقية لطول مصفوفة المهام وهي 2، ونلاحظ ذكر السطر الذي فشل عنده الاختبار ضمن ملف الاختبار وهو السطر رقم 10، وتفيد هذه المعلومات في حل المشكلة.

نعدل الاختبار ونصحح المشكلة بتوقع القيمة الصحيحة لطول المصفوفة حتى لا يفشل الاختبار، وأولًا نفتح ملف الاختبارات:

nano index.test.js

ثم نزيل أسطر إضافة المهام باستخدام todos.add ليصبح الاختبار كالتالي:

...
describe("integration test", function () {
    it("should be able to add and complete TODOs", function () {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);
    });
});

نحفظ الملف ونخرج منه ونعيد تنفيذ الاختبار مجددًا ونتأكد من نجاحه في حالة صحيحة هذه المرة وليست مغلوطة:

npm test

نحصل على الخرج:

...
integration test
     should be able to add and complete TODOs


  1 passing (15ms)

أصبح الاختبار الآن أقرب لما نريد، لنعود إلى اختبار التكامل مجددًا ونحاول اختبار إضافة مهمة جديدة ضمن الملف index.test.js كالتالي:

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);

        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
    });
});

بعد استدعاء التابع add()‎ نتحقق من وجود مهمة واحدة ضمن كائن مدير المهام todos باستخدام تابع التوكيد strictEqual()‎، وأما الاختبار التالي فسيتحقق من البيانات الموجودة ضمن قائمة المهام todos بواسطة التابع deepStrictEqual()‎ والذي يختبر مساواة القيمة المتوقعة مع القيمة الحقيقية تعاوديًا بالمرور على كل الخصائص ضمن من القيمتين واختبار مساواتهما، ففي حالتنا سيختبر أن المصفوفتين يملك كل منها كائنًا واحدًا داخلها، ويتحقق من امتلاك كلا الكائنين لنفس الخواص وتساويها ففي حالتنا يجب أن يكون هنالك خاصيتين الأولى العنوان title ويجب أن تساوي قيمتها "run code"والثاني اكتمال المهمة completed وقيمتها تساوي false.

نكمل كتابة الاختبار ليصبح كالتالي:

...
describe("integration test", function() {
    it("should be able to add and complete TODOs", function() {
        let todos = new Todos();
        assert.strictEqual(todos.list().length, 0);

        todos.add("run code");
        assert.strictEqual(todos.list().length, 1);
        assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);

        todos.add("test everything");
        assert.strictEqual(todos.list().length, 2);
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: false },
                { title: "test everything", completed: false }
            ]
        );

        todos.complete("run code");
        assert.deepStrictEqual(todos.list(),
            [
                { title: "run code", completed: true },
                { title: "test everything", completed: false }
            ]
    );
  });
});

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

والآن نحفظ الملف ونخرج منه وننفذ الاختبارات مرة أخرى ونتحقق من النتيجة:

...
integrated test
     should be able to add and complete TODOs


  1 passing (9ms)

أعددنا بذلك اختبار تكامل باستخدام إطار الاختبارات موكا Mocha والوحدة assert.

والآن لنتخيل بأننا شاركنا الوحدة البرمجية السابقة مع مطورين آخرين وأخبرنا العديد منهم بأنه يفضل رمي خطأ عند استدعاء التابع complete()‎ في حال لم يتم إضافة أي مهام بعد سابقًا، لذا لنضيف تلك الخاصية ضمن التابع complete()‎ ضمن الملف index.js:

...
complete(title) {
    if (this.todos.length === 0) {
        throw new Error("You have no TODOs stored. Why don't you add one first?");
    }

    let todoFound = false
    this.todos.forEach((todo) => {
        if (todo.title === title) {
            todo.completed = true;
            todoFound = true;
            return;
        }
    });

    if (!todoFound) {
        throw new Error(`No TODO was found with the title: "${title}"`);
    }
}
...

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

...
describe("complete()", function() {
    it("should fail if there are no TODOs", function() {
        let todos = new Todos();
        const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");

        assert.throws(() => {
            todos.complete("doesn't exist");
        }, expectedError);
    });
});

استخدما التوابع describe()‎ و it()‎ كما فعلنا سابقًا، وبدأنا الاختبار بإنشاء كائن todos جديد، ثم عرّفنا الخطأ المتوقع عند استدعاء التابع complete()‎ واستخدمنا تابع توكيد رمي الأخطاء throws()‎ الذي توفره الوحدة assert لاختبار الأخطاء المرمية من قبل الشيفرة عند تنفيذها، حيث نمرر له كمعامل أول تابعًا يحتوي داخله على التابع الذي نتوقع منه رمي الخطأ، والمعامل الثاني هو الخطأ المتوقع رميه، والآن ننفذ أمر الاختبار npm test ونعاين النتيجة:

...
integrated test
     should be able to add and complete TODOs

  complete()
     should fail if there are no TODOs


  2 passing (25ms)

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

وإلى الآن كل ما اختبرناه كان عبارة عن شيفرات متزامنة، وفي الفقرة التالية سنتعلم طرق اختبار والتعامل مع الشيفرات اللامتزامنة.

اختبار الشيفرات اللامتزامنة

سنضيف الآن ميزة تصدير قائمة المهام إلى ملف بصيغة CSV التي ذكرناها سابقًا، حيث سيحوي ذلك الملف كل المهام المخزنة مع تفاصيل حالة اكتمالها، لذا وللتعامل مع نظام الملفات سنحتاج لاستخدام وحدة fs التي توفرها نود لكتابة ذلك الملف.

والجدير بالذكرأن عملية كتابة الملف عملية غير متزامنة ويمكن تنفيذها بعدة طرق كاستخدام دوال رد النداء callbacks مثلًا أو الوعود Promises أو عبر اللاتزامن والانتظار async/await كما رأينا في المقال السابق.

سنتعلم في هذه الفقرة كيف يمكن كتابة الاختبارات للشيفرات اللامتزامنة التي تستخدم أي طريقة من تلك الطرق.

الاختبار باستخدام دوال رد النداء

تمُرر دالة رد النداء كمعامل إلى التابع اللامتزامن لتُستدعى عند انتهاء مهمة ذلك التابع، لنبدأ بإضافة التابع saveToFile()‎ للصنف Todos والذي سيمر على عناصر المهام ضمن الصنف ويبنى منها سلسلة نصية ويخزنها ضمن ملف بصيغة CSV، لذا نعود إلى ملف index.js ونضيف الشيفرات المكتوبة في نهايته:

const fs = require('fs');

class Todos {
    constructor() {
        this.todos = [];
    }

    list() {
        return [...this.todos];
    }

    add(title) {
        let todo = {
            title: title,
            completed: false,
        }
        this.todos.push(todo);
    }

    complete(title) {
        if (this.todos.length === 0) {
            throw new Error("You have no TODOs stored. Why don't you add one first?");
        }

        let todoFound = false
        this.todos.forEach((todo) => {
            if (todo.title === title) {
                todo.completed = true;
                todoFound = true;
                return;
            }
        });

        if (!todoFound) {
            throw new Error(`No TODO was found with the title: "${title}"`);
        }
    }

    saveToFile(callback) {
        let fileContents = 'Title,Completed\n';
        this.todos.forEach((todo) => {
            fileContents += `${todo.title},${todo.completed}\n`
        });

        fs.writeFile('todos.csv', fileContents, callback);
    }
}

module.exports = Todos;

بداية نستورد الوحدة fs ثم نضيف التابع الجديد saveToFile()‎ إلى الصنف والذي يقبل كمعامل له دالة رد نداء تُستدعَى عند انتهاء عملية كتابة الملف، ونُنشئ ضمن التابع الجديد محتوى الملف ونخزنه ضمن المتغير fileContents، ونلاحظ كيف عيّنا القيمة الابتدائية له وهي عناوين الأعمدة للجدول في ملف CSV، ومررنا على كل مهمة مخزنة ضمن المصفوفة باستخدام التابع forEach()‎ وأضفنا لكل مهمة قيمة خاصية العنوان لها title وحالة الاكتمال completed، ثم استدعينا التابع writeFile()‎ من وحدة fs لكتابة الملف النهائي، ومررنا له اسم الملف الناتج todos.csv وكمعامل ثانِ مررنا محتوى ذلك الملف وهو قيمة المتغير fileContents السابق، وآخر معامل هو دالة رد النداء لمعالجة الخطأ الذي قد يحدث عند تنفيذ هذه العملية.

والآن نحفظ الملف ونخرج منه ونكتب اختبارًا للتابع الجديد saveToFile()‎ يتحقق من وجود الملف المصدَّر ثم يتحقق من صحة محتواه، لذا نعود لملف الاختبار index.test.js ونبدأ بتحميل الوحدة fs في بداية الملف والتي سنستخدمها في عملية الاختبار:

const Todos = require('./index');
const assert = require('assert').strict;
const fs = require('fs');
...

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

...
describe("saveToFile()", function() {
    it("should save a single TODO", function(done) {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.saveToFile((err) => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
            done(err);
        });
    });
});

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

ونلاحظ أننا أنشأنا كائنًا جديدًا من الصنف Todos وأضفنا مهمة جديدة له بعدها استدعينا التابع الجديد saveToFile()‎ ومررنا له دالة لفحص كائن الخطأ الذي سيمرر لها إن وجد، ونلاحظ كيف وضعنا الاختبار لهذا التابع ضمن دالة رد النداء، لأن الاختبار سيفشل إن أجري قبل عملية كتابة الملف.

أول توكيد تحققنا منه هو أن الملف todos.csv موجود:

...
assert.strictEqual(fs.existsSync('todos.csv'), true);
...

حيث يعيد التابع fs.existsSync()‎ القيمة الصحيحة true إذا كان الملف المحدد بالمسار الممر له موجودًا وإلا سيعيد قيمة خاطئة false.

ملاحظة: يرجع العمل أن توابع الوحدة fs غير متزامنة افتراضيًا، ويوجد لبعض التوابع الأساسية منها نسخ متزامنة استخدمناها هنا لتبسيط الاختبار ويمكننا الاستدلال على تلك التوابع المتزامنة من اللاحقة "Sync" في نهاية اسمها، فلو استخدمنا النسخة المتزامنة ومررنا لها دالة رد نداء أيضًا فستصبح الشيفرة متداخلة وصعبة القراءة أو التعديل.

أنشأنا بعد ذلك متغيرًا يحوي القيمة المتوقعة للملف todos.csv:

...
let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
...

واستخدمنا التابع المتزامن readFileSync()‎ من الوحدة fs لقراءة محتوى الملف كالتالي:

...
let content = fs.readFileSync("todos.csv").toString();
...

حيث مررنا للتابع readFileSync()‎ مسار الملف todos.csv الذي جرى تصديره، وسيعيد لنا كائن تخزين مؤقت Buffer سيحوي بيانات الملف بالصيغة الثنائية، لذا نستدعي التابع toString()‎ منه للحصول على القيمة النصية المقروءة لتلك البيانات لمقارنتها مع القيمة المتوقعة لمحتوى الملف التي أنشأناها مسبقًا، ونستخدم لمقارنتهما تابع اختبار المساواة strictEqual من الوحدة assert:

...
assert.strictEqual(content, expectedFileContents);
...

وأخيرًا نستدعي التابع done()‎ لإعلام موكا بانتهاء الاختبار:

...
done(err);
...

نلاحظ كيف مررنا كائن الخطأ err عند استدعاء تابع الانتهاء done()‎ حيث سيفحص موكا تلك القيمة وسيفشل الاختبار إن احتوت على خطأ.

والآن نحفظ الملف ونخرج منه وننفذ الاختبارات بتنفيذ التابع npm test كما العادة ونلاحظ النتيجة:

...
integrated test
     should be able to add and complete TODOs

  complete()
     should fail if there are no TODOs

  saveToFile()
     should save a single TODO


  3 passing (15ms)

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

الاختبار باستخدام الوعود

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

لنبدأ بتعديل التابع saveToFile()‎ ليستخدم الوعود بدلًا من دوال رد النداء، نفتح ملف index.js ونبدأ بتعديل طريقة استيراد الوحدة fs، حيث نعدل على عبارة الاستيراد باستخدام require()‎ لتصبح كالتالي:

...
const fs = require('fs').promises;
...

بذلك نكون قد استوردنا وحدة fs التي تستخدم الوعود بدلًا من التي تستخدم دوال رد النداء، ثم نعدل التابع saveToFile()‎ ليستخدم الوعود بشكل سليم كالتالي:

...
saveToFile() {
    let fileContents = 'Title,Completed\n';
    this.todos.forEach((todo) => {
        fileContents += `${todo.title},${todo.completed}\n`
    });

    return fs.writeFile('todos.csv', fileContents);
}
...

نلاحظ أن التابع لم يعد يقبل معاملًا له، حيث يغنينا استخدام الوعود عن ذلك، ونلاحظ أيضًا تغيير طريقة كتابة التابع حيث نرجع منه الوعد الذي يرجعه التابع writeFile()‎.

والآن نحفظ التغييرات على ملف index.js ثم نعدل على اختبار هذا التابع ليلائم استخدامه للوعود، لذا نعود لملف الاختبار index.test.js ونبدل اختبار التابع saveToFile()‎ ليصبح كالتالي:

...
describe("saveToFile()", function() {
    it("should save a single TODO", function() {
        let todos = new Todos();
        todos.add("save a CSV");
        return todos.saveToFile().then(() => {
            assert.strictEqual(fs.existsSync('todos.csv'), true);
            let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
            let content = fs.readFileSync("todos.csv").toString();
            assert.strictEqual(content, expectedFileContents);
        });
    });
});

أول تعديل أجريناه هو إزالة معامل تابع الانتهاء done()‎ لأن بقاءه يعني انتظار موكا إشارة استدعائه حتى ينهي الاختبار وإلا سيرمي خطأ كالتالي:

1) saveToFile()
       should save a single TODO:
     Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
      at listOnTimeout (internal/timers.js:536:17)
      at processTimers (internal/timers.js:480:7)

لهذا السبب عندما نستخدم الوعود ضمن الاختبار لا نمرر المعامل done()‎ إلى دالة رد النداء المُمررة لدالة تعريف الاختبار it()‎.

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

والآن وبعد أن عدلنا الاختبار نحفظ الملف ونخرج منه، وننفذ الأمر npm test لتنفيذ الاختبارات والتأكد من نجاحها:

...
integrated test
     should be able to add and complete TODOs

  complete()
     should fail if there are no TODOs

  saveToFile()
     should save a single TODO


  3 passing (18ms)

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

الاختبار باستخدام اللاتزامن والانتظار async/await

تتيح الكلمتان المفتاحيتان async/await صيغة بديلة للتعامل مع الوعود، فعند تحديد تابع ما كتابع لا متزامن باستخدام الكلمة المفتاحية async يصبح بإمكاننا الحصول داخله مباشرةً على قيمة نتيجة أي وعد ننفذه عند نجاحه باستخدام الكلمة المفتاحية await قبل استدعاء الوعد، وبذلك نلغي الحاجة لاستدعاء التوابع then()‎ أو catch()‎ نهائيًا، وباستخدامها يمكننا تبسيط اختبار التابع saveToFile()‎ الذي يستخدم الوعود، لذا نعدله ضمن ملف الاختبارات index.test.js ليصبح كالتالي:

...
describe("saveToFile()", function() {
    it("should save a single TODO", async function() {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

نلاحظ كيف أضفنا الكلمة async قبل تعريف دالة رد النداء المُمرر إلى it()‎، ما يسمح لنا باستخدام الكلمة await داخلها، ونلاحظ عند استدعاء التابع saveToFile()‎ إضافة الكلمة await قبل استدعائه بذلك لن يكمل نود تنفيذ الشيفرات في الأسطر اللاحقة وسينتظر لحين انتهاء تنفيذ هذا التابع، ونلاحظ أيضًا كيف أصبحت شيفرة الاختبار أسهل في القراءة بعد أن نقلنا شيفرات التوكيد من داخل التابع then()‎ مباشرة إلى جسم تابع الاختبار المُمرر إلى it()‎.

والآن ننفذ الاختبارات بتنفيذ الأمر npm test لنحصل على الخرج:

...
integrated test
     should be able to add and complete TODOs

  complete()
     should fail if there are no TODOs

  saveToFile()
     should save a single TODO


  3 passing (30ms)

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

تحسين الاختبارات باستخدام الخطافات Hooks

تسمح الخطافات في موكا Mocha بإعداد بيئة الاختبار قبل وبعد تنفيذ الاختبارات، حيث نستخدمها داخل التابع describe()‎ عادةً وتحوي على شيفرات تفيد في عملية الإعداد والتنظيف التي قد تحتاجها بعض الاختبارات، حيث يوفر موكا أربع خطافات رئيسية وهي:

  • before: يُنفذ مرة واحدة قبل أول اختبار فقط.
  • beforeEach: يُنفذ قبل كل اختبار.
  • after: يُنفذ بعد تنفيذ آخر اختبار فقط.
  • afterEach: يُنفذ بعد كل اختبار.

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

لنبدأ بإضافة اختبار ثانِ للتأكد من حفظ المهام المكتملة بشكل سليم، لذا نفتح الملف index.test.js ضمن محرر النصوص ونضيف الاختبار الجديد كالتالي:

...
describe("saveToFile()", function () {
    it("should save a single TODO", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        let todos = new Todos();
        todos.add("save a CSV");
        todos.complete("save a CSV");
        await todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

يشبه هذا الاختبار ما سبقه، والفرق الوحيد هو استدعاء التابع complete()‎ قبل تصدير الملف باستخدام التابع saveToFile()‎، وأيضًا اختلاف محتوى الملف المتوقع ضمن المتغير expectedFileContents حيث يحوي القيمة true بدلًا من false عند حقل حالة الاكتمال للمهمة completed.

والآن نحفظ الملف ونخرج منه وننفذ الاختبارات بتنفيذ الأمر:

npm test

سيظهر لنا التالي:

...
integrated test
     should be able to add and complete TODOs

  complete()
     should fail if there are no TODOs

  saveToFile()
     should save a single TODO
     should save a single TODO that's completed


  4 passing (26ms)

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

وقد يظن المستخدم لهذه الوحدة خطأً أن هذا الملف هو ملف مهام حقيقية وليس ملف ناتج عن عملية الاختبار، ولحل تلك المشكلة يمكننا حذف الملفات الناتجة بعد انتهاء الاختبار مباشرةً باستخدام الخطافات تلك، حيث نستفيد من الخطاف beforeEach()‎ لإعداد المهام قبل اختبارها، وهنا ضمن هذا الخطاف نُعد ونحضر عادةً أي بيانات سنستخدمها داخل الاختبارات، ففي حالتنا نريد إنشاء الكائن todos وبداخله مهمة جديدة نجهزها ونضيفها مسبقًا، وسنستفيد من الخطاف afterEach()‎ لحذف الملفات الناتجة بعد كل اختبار، لذلك نعدل مجموعة اختبارات التابع saveToFile()‎ ضمن ملف الاختبارات index.test.js ليصبح كالتالي:

...
describe("saveToFile()", function () {
    beforeEach(function () {
        this.todos = new Todos();
        this.todos.add("save a CSV");
    });

    afterEach(function () {
        if (fs.existsSync("todos.csv")) {
            fs.unlinkSync("todos.csv");
        }
    });

    it("should save a single TODO without error", async function () {
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync("todos.csv"), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,false\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });

    it("should save a single TODO that's completed", async function () {
        this.todos.complete("save a CSV");
        await this.todos.saveToFile();

        assert.strictEqual(fs.existsSync('todos.csv'), true);
        let expectedFileContents = "Title,Completed\nsave a CSV,true\n";
        let content = fs.readFileSync("todos.csv").toString();
        assert.strictEqual(content, expectedFileContents);
    });
});

نلاحظ إضافة الخطاف beforeEach()‎ داخل مجموعة الاختبار:

...
beforeEach(function () {
    this.todos = new Todos();
    this.todos.add("save a CSV");
});
...

حيث أنشأنا كائنًا جديدًا من الصنف Todos سيكون متاحًا لكل الاختبارات ضمن هذه المجموعة، وذلك لأن موكا سيشارك قيمة الكائن this الذي أضفنا له خصائص ضمن الخطاف beforeEach()‎ مع جميع الاختبارات في توابع الاختبار it()‎، وقيمته ستكون واحدة ضمن مجموعة الاختبارات داخل describe()‎، حيث بالاستفادة من تلك الميزة يمكننا مشاركة بيانات مُعدة مسبقًا مع جميع الاختبارات.

أما داخل الخطاف afterEach()‎، فقد حذفنا ملف CSV الناتج عن الاختبارات:

...
afterEach(function () {
    if (fs.existsSync("todos.csv")) {
        fs.unlinkSync("todos.csv");
    }
});
...

في حال فشلت الاختبارات فلن يُنشأ ذلك الملف، لهذا السبب نختبر أولًا وجوده باستخدام التابع existsSync()‎ قبل تنفيذ عملية الحذف باستخدام التابع unlinkSync()‎، ثم بدلنا في باقي الاختبارات الإشارة إلى كائن المهام todos الذي كنا نُنشئه ضمن it()‎ مباشرةً، ليشير إلى الكائن الذي أعددناه ضمن الخطاف عن طريق this.todos، وحذفنا أسطر إنشاء الكائن todos ضمن تلك الاختبارات.

والآن لننفذ تلك الاختبارات بعد التعديلات ونتأكد من نتيجتها بتنفيذ الأمر npm test لنحصل على التالي:

...
integrated test
     should be able to add and complete TODOs

  complete()
     should fail if there are no TODOs

  saveToFile()
     should save a single TODO without error
     should save a single TODO that's completed


  4 passing (20ms)

نلاحظ أنه لا تغيير في نتائج الاختبار وجميعها نجحت، وأصبحت اختبارات التابع saveToFile()‎ أبسط وأسرع بسبب مشاركة الكائن مع جميع الاختبارات، وحللنا مشكلة ملف CSV الناتج عن تنفيذ الاختبارات.

خاتمة

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

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

ترجمة -وبتصرف- للمقال How To Test a Node.js Module with Mocha and Assert لصاحبه Stack Abuse.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...