كتابة كود قابل للاختبار في JavaScript: نظرة عامة موجزة
نشرت: 2022-03-11سواء كنا نستخدم Node مقترنًا بإطار عمل اختباري مثل Mocha أو Jasmine ، أو نقوم بتدوير الاختبارات المعتمدة على DOM في متصفح بدون رأس مثل PhantomJS ، فإن خياراتنا لاختبار وحدات JavaScript أفضل الآن من أي وقت مضى.
ومع ذلك ، هذا لا يعني أن الكود الذي نختبره سهل علينا مثل أدواتنا! يتطلب تنظيم وكتابة التعليمات البرمجية التي يمكن اختبارها بسهولة بعض الجهد والتخطيط ، ولكن هناك بعض الأنماط المستوحاة من مفاهيم البرمجة الوظيفية ، والتي يمكننا استخدامها لتجنب الوقوع في موقف صعب عندما يحين وقت اختبار الكود الخاص بنا. في هذه المقالة ، سنتعرف على بعض النصائح والأنماط المفيدة لكتابة تعليمات برمجية قابلة للاختبار في JavaScript.
حافظ على منطق الأعمال ومنطق العرض منفصلين
تتمثل إحدى الوظائف الأساسية لتطبيق المستعرض المستند إلى JavaScript في الاستماع إلى أحداث DOM التي يطلقها المستخدم النهائي ، ثم الاستجابة لها عن طريق تشغيل بعض منطق الأعمال وعرض النتائج على الصفحة. من المغري كتابة دالة مجهولة تؤدي الجزء الأكبر من العمل بشكل صحيح حيث تقوم بإعداد مستمعين لحدث DOM. المشكلة التي يخلقها هذا هو أنه عليك الآن محاكاة أحداث DOM لاختبار وظيفتك المجهولة. يمكن أن يؤدي ذلك إلى إنشاء عبء في سطور التعليمات البرمجية والوقت الذي يستغرقه تشغيل الاختبارات.
بدلاً من ذلك ، اكتب دالة مسماة ومررها إلى معالج الأحداث. وبهذه الطريقة يمكنك كتابة اختبارات للوظائف المسماة مباشرةً ودون القفز عبر الأطواق لإطلاق حدث DOM مزيف.
هذا ينطبق على أكثر من DOM بالرغم من ذلك. تم تصميم العديد من واجهات برمجة التطبيقات ، سواء في المستعرض أو في Node ، حول إطلاق الأحداث والاستماع إليها أو انتظار إكمال أنواع أخرى من العمل غير المتزامن. القاعدة الأساسية هي أنه إذا كنت تكتب الكثير من وظائف رد الاتصال المجهولة ، فقد لا يكون من السهل اختبار الكود الخاص بك.
// hard to test $('button').on('click', () => { $.getJSON('/path/to/data') .then(data => { $('#my-list').html('results: ' + data.join(', ')); }); }); // testable; we can directly run fetchThings to see if it // makes an AJAX request without having to trigger DOM // events, and we can run showThings directly to see that it // displays data in the DOM without doing an AJAX request $('button').on('click', () => fetchThings(showThings)); function fetchThings(callback) { $.getJSON('/path/to/data').then(callback); } function showThings(data) { $('#my-list').html('results: ' + data.join(', ')); }
استخدام الاسترجاعات أو الوعود مع التعليمات البرمجية غير المتزامنة
في مثال الكود أعلاه ، تقوم وظيفة fetchThings المعاد بناؤها بتشغيل طلب AJAX ، والذي يقوم بمعظم عمله بشكل غير متزامن. هذا يعني أنه لا يمكننا تشغيل الوظيفة واختبار أنها فعلت كل ما توقعناه ، لأننا لن نعرف متى تنتهي من العمل.
الطريقة الأكثر شيوعًا لحل هذه المشكلة هي تمرير وظيفة رد الاتصال كمعامل إلى الوظيفة التي تعمل بشكل غير متزامن. في اختبارات الوحدة الخاصة بك ، يمكنك تشغيل تأكيداتك في رد الاتصال الذي تجتازه.
هناك طريقة أخرى شائعة وشائعة بشكل متزايد لتنظيم التعليمات البرمجية غير المتزامنة وهي Promise API. لحسن الحظ ، فإن $ .ajax ومعظم وظائف jQuery غير المتزامنة الأخرى ترجع كائن Promise بالفعل ، لذلك تمت تغطية الكثير من حالات الاستخدام الشائعة بالفعل.
// hard to test; we don't know how long the AJAX request will run function fetchData() { $.ajax({ url: '/path/to/data' }); } // testable; we can pass a callback and run assertions inside it function fetchDataWithCallback(callback) { $.ajax({ url: '/path/to/data', success: callback, }); } // also testable; we can run assertions when the returned Promise resolves function fetchDataWithPromise() { return $.ajax({ url: '/path/to/data' }); }
تجنب الآثار الجانبية
اكتب الدوال التي تأخذ وسيطات وتعيد قيمة بناءً على تلك الوسيطات فقط ، تمامًا مثل ضرب الأرقام في معادلة رياضية للحصول على نتيجة. إذا كانت وظيفتك تعتمد على بعض الحالات الخارجية (خصائص مثيل فئة أو محتويات ملف ، على سبيل المثال) ، وكان عليك إعداد هذه الحالة قبل اختبار وظيفتك ، فيجب عليك إجراء المزيد من الإعداد في اختباراتك. يجب أن تثق في أن أي كود آخر قيد التشغيل لا يغير نفس الحالة.
على نفس المنوال ، تجنب كتابة الوظائف التي تغير الحالة الخارجية (مثل الكتابة إلى ملف أو حفظ القيم في قاعدة بيانات) أثناء تشغيلها. هذا يمنع الآثار الجانبية التي قد تؤثر على قدرتك على اختبار التعليمات البرمجية الأخرى بثقة. بشكل عام ، من الأفضل الاحتفاظ بالآثار الجانبية بالقرب من حواف الكود الخاص بك قدر الإمكان ، مع أقل مساحة ممكنة من "مساحة السطح". في حالة وجود الفئات وكائنات الكائن ، يجب أن تقتصر الآثار الجانبية لطريقة الفئة على حالة مثيل الفئة التي يتم اختبارها.
// hard to test; we have to set up a globalListOfCars object and set up a // DOM with a #list-of-models node to test this code function processCarData() { const models = globalListOfCars.map(car => car.model); $('#list-of-models').html(models.join(', ')); } // easy to test; we can pass an argument and test its return value, without // setting any global values on the window or checking the DOM the result function buildModelsString(cars) { const models = cars.map(car => car.model); return models.join(','); }
استخدم حقن التبعية
أحد الأنماط الشائعة لتقليل استخدام الوظيفة للحالة الخارجية هو حقن التبعية - تمرير جميع الاحتياجات الخارجية للوظيفة كمعلمات وظيفية.

// depends on an external state database connector instance; hard to test function updateRow(rowId, data) { myGlobalDatabaseConnector.update(rowId, data); } // takes a database connector instance in as an argument; easy to test! function updateRow(rowId, data, databaseConnector) { databaseConnector.update(rowId, data); }
تتمثل إحدى الفوائد الرئيسية لاستخدام حقن التبعية في أنه يمكنك تمرير كائنات وهمية من اختبارات الوحدة الخاصة بك والتي لا تسبب آثارًا جانبية حقيقية (في هذه الحالة ، تحديث صفوف قاعدة البيانات) ويمكنك فقط التأكيد على أن الكائن الوهمي الخاص بك قد تم التعامل معه بالطريقة المتوقعة.
امنح كل وظيفة غرضًا واحدًا
قسّم الوظائف الطويلة التي تقوم بالعديد من الأشياء إلى مجموعة من الوظائف القصيرة ذات الغرض الواحد. هذا يجعل من الأسهل بكثير اختبار أن كل وظيفة تؤدي دورها بشكل صحيح ، بدلاً من الأمل في أن تقوم وظيفة كبيرة بكل شيء بشكل صحيح قبل إرجاع قيمة.
في البرمجة الوظيفية ، يُطلق على فعل توتير العديد من الوظائف أحادية الغرض معًا التركيب. يحتوي Underscore.js حتى على وظيفة _.compose
، تأخذ قائمة بالوظائف وتربطها معًا ، مع أخذ القيمة المرجعة لكل خطوة وتمريرها إلى الوظيفة التالية في السطر.
// hard to test function createGreeting(name, location, age) { let greeting; if (location === 'Mexico') { greeting = '!Hola'; } else { greeting = 'Hello'; } greeting += ' ' + name.toUpperCase() + '! '; greeting += 'You are ' + age + ' years old.'; return greeting; } // easy to test function getBeginning(location) { if (location === 'Mexico') { return 'Hola'; } else { return 'Hello'; } } function getMiddle(name) { return ' ' + name.toUpperCase() + '! '; } function getEnd(age) { return 'You are ' + age + ' years old.'; } function createGreeting(name, location, age) { return getBeginning(location) + getMiddle(name) + getEnd(age); }
لا تغير المعامِلات
في JavaScript ، يتم تمرير المصفوفات والكائنات عن طريق المرجع بدلاً من القيمة ، وهي قابلة للتغيير. هذا يعني أنه عندما تقوم بتمرير كائن أو مصفوفة كمعامل إلى دالة ، فإن كلاً من التعليمات البرمجية الخاصة بك والوظيفة التي قمت بتمريرها للكائن أو المصفوفة لديك القدرة على تغيير نفس مثيل ذلك المصفوفة أو الكائن في الذاكرة. هذا يعني أنك إذا كنت تختبر الكود الخاص بك ، فعليك أن تثق في أن أياً من الوظائف التي يستدعيها الكود الخاص بك لا تغير كائناتك. في كل مرة تضيف فيها مكانًا جديدًا في التعليمات البرمجية الخاصة بك يغير نفس الكائن ، يصبح من الصعب بشكل متزايد تتبع الشكل الذي يجب أن يبدو عليه هذا الكائن ، مما يجعل اختباره أكثر صعوبة.
بدلاً من ذلك ، إذا كان لديك دالة تأخذ كائنًا أو مصفوفة ، اجعلها تعمل على هذا الكائن أو المصفوفة كما لو كانت للقراءة فقط. قم بإنشاء كائن أو مجموعة جديدة في التعليمات البرمجية وأضف القيم إليها بناءً على احتياجاتك. أو استخدم Underscore أو Lodash لاستنساخ الكائن أو المصفوفة التي تم تمريرها قبل العمل عليها. والأفضل من ذلك ، استخدم أداة مثل Immutable.js تنشئ هياكل بيانات للقراءة فقط.
// alters objects passed to it function upperCaseLocation(customerInfo) { customerInfo.location = customerInfo.location.toUpperCase(); return customerInfo; } // sends a new object back instead function upperCaseLocation(customerInfo) { return { name: customerInfo.name, location: customerInfo.location.toUpperCase(), age: customerInfo.age }; }
اكتب الاختبارات الخاصة بك قبل التعليمات البرمجية الخاصة بك
تسمى عملية كتابة اختبارات الوحدة قبل الكود الذي يقومون باختباره بالتطوير المدفوع بالاختبار (TDD). يجد الكثير من المطورين أن TDD مفيدة جدًا.
من خلال كتابة اختباراتك أولاً ، فأنت مجبر على التفكير في واجهة برمجة التطبيقات التي تعرضها من منظور مطور يستهلكها. كما أنه يساعد على التأكد من أنك تكتب فقط ما يكفي من التعليمات البرمجية للوفاء بالعقد الذي تفرضه اختباراتك ، بدلاً من المبالغة في هندسة حل معقد بشكل غير ضروري.
من الناحية العملية ، يعد TDD نظامًا قد يكون من الصعب الالتزام به لجميع تغييرات التعليمات البرمجية الخاصة بك. ولكن عندما يبدو الأمر يستحق المحاولة ، فهذه طريقة رائعة لضمان الحفاظ على جميع التعليمات البرمجية قابلة للاختبار.
يتم إحتوائه
نعلم جميعًا أن هناك بعض المزالق التي يسهل الوقوع فيها عند كتابة تطبيقات JavaScript المعقدة واختبارها. ولكن نأمل من خلال هذه النصائح ، وتذكر الحفاظ دائمًا على الكود الخاص بنا بسيطًا وعمليًا قدر الإمكان ، يمكننا الحفاظ على تغطية الاختبار عالية وتعقيد الكود بشكل عام منخفض!
- الأخطاء العشرة الأكثر شيوعًا التي يرتكبها مطورو JavaScript
- الحاجة إلى السرعة: تحدي ترميز جافا سكريبت Toptal بأثر رجعي