دليل Node.js لإجراء اختبارات التكامل فعليًا
نشرت: 2022-03-11اختبارات التكامل ليست شيئًا يجب أن يكون مخيفًا. إنها جزء أساسي من اختبار التطبيق الخاص بك بالكامل.
عندما نتحدث عن الاختبار ، عادة ما نفكر في اختبارات الوحدة حيث نختبر جزءًا صغيرًا من التعليمات البرمجية بمعزل عن غيرها. ومع ذلك ، فإن تطبيقك أكبر من ذلك الجزء الصغير من التعليمات البرمجية ولا يعمل أي جزء من تطبيقك بمعزل عن غيره. هذا هو المكان الذي تثبت فيه اختبارات التكامل أهميتها. تلتقط اختبارات التكامل المكان الذي تقصر فيه اختبارات الوحدة ، وتقوم بسد الفجوة بين اختبارات الوحدة والاختبارات الشاملة.
في هذه المقالة ، ستتعلم كيفية كتابة اختبارات تكامل قابلة للقراءة والتركيب باستخدام أمثلة في التطبيقات المستندة إلى واجهة برمجة التطبيقات.
بينما سنستخدم JavaScript / Node.js لجميع أمثلة التعليمات البرمجية في هذه المقالة ، يمكن تكييف معظم الأفكار التي تمت مناقشتها بسهولة مع اختبارات التكامل على أي نظام أساسي.
اختبارات الوحدة مقابل اختبارات التكامل: أنت بحاجة إلى كليهما
تركز اختبارات الوحدة على وحدة معينة من التعليمات البرمجية. غالبًا ما تكون هذه طريقة محددة أو وظيفة لمكون أكبر.
يتم إجراء هذه الاختبارات بشكل منفصل ، حيث يتم عادةً إهانة جميع التبعيات الخارجية أو السخرية منها.
بمعنى آخر ، يتم استبدال التبعيات بسلوك مبرمج مسبقًا ، مما يضمن أن نتيجة الاختبار يتم تحديدها فقط من خلال صحة الوحدة التي يتم اختبارها.
يمكنك معرفة المزيد عن اختبارات الوحدة هنا.
تستخدم اختبارات الوحدة للحفاظ على كود عالي الجودة بتصميم جيد. كما أنها تتيح لنا تغطية حالات الزاوية بسهولة.
ومع ذلك ، فإن العيب هو أن اختبارات الوحدة لا يمكن أن تغطي التفاعل بين المكونات. هذا هو المكان الذي تصبح فيه اختبارات التكامل مفيدة.
اختبارات التكامل
إذا تم تحديد اختبارات الوحدة عن طريق اختبار أصغر وحدات الكود بمعزل عن غيرها ، فإن اختبارات التكامل تكون عكس ذلك تمامًا.
تُستخدم اختبارات التكامل لاختبار وحدات (مكونات) متعددة وأكبر في التفاعل ، ويمكن أن تمتد أحيانًا إلى أنظمة متعددة.
الغرض من اختبارات التكامل هو العثور على أخطاء في الاتصالات والتبعيات بين المكونات المختلفة ، مثل:
- تمرير الحجج غير الصحيحة أو مرتبة بشكل غير صحيح
- مخطط قاعدة البيانات معطل
- تكامل ذاكرة التخزين المؤقت غير صالح
- عيوب في منطق الأعمال أو أخطاء في تدفق البيانات (لأن الاختبار يتم الآن من منظور أوسع).
إذا كانت المكونات التي نختبرها لا تحتوي على أي منطق معقد (مثل المكونات ذات الحد الأدنى من التعقيد الدوري) ، فستكون اختبارات التكامل أكثر أهمية من اختبارات الوحدة.
في هذه الحالة ، سيتم استخدام اختبارات الوحدة بشكل أساسي لفرض تصميم كود جيد.
بينما تساعد اختبارات الوحدة في ضمان كتابة الوظائف بشكل صحيح ، تساعد اختبارات التكامل في ضمان عمل النظام بشكل صحيح ككل. لذا فإن كلا من اختبارات الوحدة واختبارات التكامل يخدم كل منهما غرضه التكميلي ، وكلاهما ضروري لنهج اختبار شامل.
تشبه اختبارات الوحدة واختبارات التكامل وجهين لعملة واحدة. العملة غير صالحة بدون كليهما.
لذلك ، لا يكتمل الاختبار حتى تكمل كلاً من اختبارات التكامل والوحدة.
قم بإعداد Suite for Integration Tests
بينما يعد إعداد مجموعة اختبار لاختبارات الوحدة أمرًا بسيطًا جدًا ، فإن إعداد مجموعة اختبار لاختبارات التكامل يكون في كثير من الأحيان أكثر صعوبة.
على سبيل المثال ، يمكن أن تحتوي المكونات في اختبارات التكامل على تبعيات خارج المشروع ، مثل قواعد البيانات وأنظمة الملفات وموفري البريد الإلكتروني وخدمات الدفع الخارجية وما إلى ذلك.
في بعض الأحيان ، تحتاج اختبارات التكامل إلى استخدام هذه الخدمات والمكونات الخارجية ، وفي بعض الأحيان يمكن إيقافها.
عندما تكون هناك حاجة إليها ، يمكن أن يؤدي ذلك إلى العديد من التحديات.
- تنفيذ الاختبار الهش: يمكن أن تكون الخدمات الخارجية غير متاحة أو تُرجع استجابة غير صالحة أو تكون في حالة غير صالحة. في بعض الحالات ، قد يؤدي هذا إلى نتيجة إيجابية خاطئة ، وفي أحيان أخرى قد يؤدي إلى نتيجة سلبية خاطئة.
- التنفيذ البطيء: يمكن أن يكون التحضير للخدمات الخارجية والاتصال بها بطيئًا. عادة ، يتم إجراء الاختبارات على خادم خارجي كجزء من CI.
- إعداد الاختبار المعقد: يجب أن تكون الخدمات الخارجية في الحالة المطلوبة للاختبار. على سبيل المثال ، يجب أن تكون قاعدة البيانات محملة مسبقًا ببيانات الاختبار المطلوبة ، إلخ.
توجيهات لمتابعة أثناء كتابة اختبارات التكامل
لا تحتوي اختبارات التكامل على قواعد صارمة مثل اختبارات الوحدة. بالرغم من ذلك ، هناك بعض التوجيهات العامة التي يجب اتباعها عند كتابة اختبارات التكامل.
الاختبارات المتكررة
يجب ألا يغير ترتيب الاختبار أو التبعيات نتيجة الاختبار. يجب أن يؤدي إجراء نفس الاختبار عدة مرات إلى إرجاع نفس النتيجة دائمًا. قد يكون من الصعب تحقيق ذلك إذا كان الاختبار يستخدم الإنترنت للاتصال بخدمات الجهات الخارجية. ومع ذلك ، يمكن حل هذه المشكلة من خلال الاستهزاء والسخرية.
بالنسبة إلى التبعيات الخارجية التي لديك قدر أكبر من التحكم فيها ، فإن إعداد الخطوات قبل اختبار التكامل وبعده سيساعد في ضمان تشغيل الاختبار دائمًا بدءًا من حالة مماثلة.
اختبار الإجراءات ذات الصلة
لاختبار جميع الحالات الممكنة ، تعد اختبارات الوحدة خيارًا أفضل بكثير.
تكون اختبارات التكامل أكثر توجهاً نحو الاتصال بين الوحدات ، ومن ثم فإن اختبار السيناريوهات السعيدة عادة ما يكون هو السبيل للذهاب لأنه سيغطي الروابط المهمة بين الوحدات.
اختبار وتأكيد مفهومين
يجب أن تُعلم إحدى النظرات السريعة للاختبار القارئ بما يتم اختباره ، وكيف يتم إعداد البيئة ، وما الذي تم إيقافه ، ومتى يتم تنفيذ الاختبار ، وما تم التأكيد عليه. يجب أن تكون التأكيدات بسيطة وأن تستخدم المساعدين لإجراء مقارنة وتسجيل أفضل.
سهل الإعداد للاختبار
يجب أن يكون إجراء الاختبار إلى الحالة الأولية بسيطًا ومفهومًا قدر الإمكان.
تجنب اختبار كود الطرف الثالث
بينما يمكن استخدام خدمات الجهات الخارجية في الاختبارات ، فلا داعي لاختبارها. وإذا كنت لا تثق بهم ، فمن المحتمل ألا تستخدمهم.
اترك كود الإنتاج خاليًا من كود الاختبار
يجب أن يكون كود الإنتاج نظيفًا ومباشرًا. سيؤدي خلط كود الاختبار مع كود الإنتاج إلى اقتران مجالين غير قابلين للاتصال معًا.
التسجيل ذات الصلة
الاختبارات الفاشلة ليست ذات قيمة كبيرة بدون تسجيل جيد.
عندما تجتاز الاختبارات ، لا يلزم تسجيل إضافي. ولكن عندما تفشل ، فإن قطع الأشجار على نطاق واسع أمر حيوي.
يجب أن يحتوي التسجيل على جميع استعلامات قاعدة البيانات وطلبات واجهة برمجة التطبيقات واستجاباتها ، بالإضافة إلى مقارنة كاملة لما يتم تأكيده. هذا يمكن أن يسهل بشكل كبير التصحيح.
تبدو الاختبارات الجيدة نظيفة ومفهومة
يمكن أن يبدو الاختبار البسيط الذي يتبع الإرشادات الواردة هنا كما يلي:
const co = require('co'); const test = require('blue-tape'); const factory = require('factory'); const superTest = require('../utils/super_test'); const testEnvironment = require('../utils/test_environment_preparer'); const path = '/v1/admin/recipes'; test(`API GET ${path}`, co.wrap(function* (t) { yield testEnvironment.prepare(); const recipe1 = yield factory.create('recipe'); const recipe2 = yield factory.create('recipe'); const serverResponse = yield superTest.get(path); t.deepEqual(serverResponse.body, [recipe1, recipe2]); }));
يختبر الكود أعلاه واجهة برمجة تطبيقات ( GET /v1/admin/recipes
) تتوقع منها إرجاع مجموعة من الوصفات المحفوظة كاستجابة.
يمكنك أن ترى أن الاختبار ، بقدر ما هو بسيط ، يعتمد على الكثير من المرافق. هذا أمر شائع لأي مجموعة اختبار تكامل جيدة.
تجعل المكونات المساعدة من السهل كتابة اختبارات تكامل مفهومة.
دعنا نراجع المكونات المطلوبة لاختبار التكامل.
مكونات المساعد
تحتوي مجموعة الاختبارات الشاملة على عدد قليل من المكونات الأساسية ، بما في ذلك: التحكم في التدفق وإطار الاختبار ومعالج قاعدة البيانات وطريقة للاتصال بواجهات برمجة التطبيقات الخلفية.
التحكم في التدفق
أحد أكبر التحديات في اختبار JavaScript هو التدفق غير المتزامن.
يمكن أن تؤدي عمليات الاسترداد إلى إحداث فوضى في الشفرة والوعود ليست كافية. هذا هو المكان الذي تصبح فيه مساعدي التدفق مفيدة.
أثناء انتظار عدم التزامن / انتظار الدعم الكامل ، يمكن استخدام المكتبات ذات السلوك المماثل. الهدف هو كتابة رمز قابل للقراءة ومعبّر وقوي مع إمكانية وجود تدفق غير متزامن.
تمكن Co من كتابة التعليمات البرمجية بطريقة لطيفة بينما تحافظ عليها بدون حظر. يتم ذلك من خلال تحديد وظيفة التوليد المشترك ثم تحقيق النتائج.
حل آخر هو استخدام Bluebird. Bluebird هي مكتبة وعد بها ميزات مفيدة جدًا مثل التعامل مع المصفوفات والأخطاء والوقت وما إلى ذلك.

يتصرف Co و Bluebird coroutine بشكل مشابه لـ async / wait في ES7 (في انتظار الحل قبل المتابعة) ، والفرق الوحيد هو أنه سيعيد دائمًا الوعد ، وهو أمر مفيد للتعامل مع الأخطاء.
إطار الاختبار
اختيار إطار الاختبار يأتي فقط إلى التفضيل الشخصي. أفضّل هو إطار عمل سهل الاستخدام ، وليس له آثار جانبية ، ويمكن قراءته بسهولة وتوصيله بالأنابيب.
هناك مجموعة واسعة من أطر الاختبار في JavaScript. في أمثلةنا ، نستخدم الشريط. لا يفي الشريط ، في رأيي ، بهذه المتطلبات فحسب ، بل إنه أيضًا أنظف وأبسط من أطر الاختبار الأخرى مثل Mocha أو Jasmin.
يعتمد الشريط على بروتوكول اختبار أي شيء (TAP).
يحتوي TAP على اختلافات لمعظم لغات البرمجة.
يأخذ Tape الاختبارات كمدخلات ، ويقوم بتشغيلها ، ثم يُخرج النتائج على شكل TAP. يمكن بعد ذلك نقل نتيجة TAP إلى مراسل الاختبار أو يمكن إخراجها إلى وحدة التحكم بتنسيق خام. يتم تشغيل الشريط من سطر الأوامر.
يحتوي الشريط على بعض الميزات الرائعة ، مثل تحديد وحدة ليتم تحميلها قبل تشغيل مجموعة الاختبار بأكملها ، وتوفير مكتبة تأكيد صغيرة وبسيطة ، وتحديد عدد التأكيدات التي يجب استدعاؤها في الاختبار. يمكن أن يؤدي استخدام وحدة للتحميل المسبق إلى تبسيط إعداد بيئة الاختبار وإزالة أي كود غير ضروري.
مكتبة المصنع
تتيح لك مكتبة المصنع استبدال ملفات التثبيت الثابتة بطريقة أكثر مرونة لتوليد البيانات للاختبار. تتيح لك هذه المكتبة تحديد النماذج وإنشاء كيانات لتلك النماذج دون كتابة تعليمات برمجية معقدة وفوضوية.
جافا سكريبت بها factory_girl لهذا - مكتبة مستوحاة من جوهرة تحمل اسمًا مشابهًا ، والتي تم تطويرها في الأصل لـ Ruby on Rails.
const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, { username: 'Bob', number_of_recipes: 50 }); const user = factory.build('user');
للبدء ، يجب تحديد نموذج جديد في factory_girl.
تم تحديده باسم ، ونموذج من مشروعك ، وكائن يتم إنشاء مثيل جديد منه.
بدلاً من ذلك ، بدلاً من تحديد الكائن الذي يتم إنشاء مثيل جديد منه ، يمكن توفير وظيفة تعيد كائنًا أو وعدًا.
عند إنشاء مثيل جديد للنموذج ، يمكننا:
- تجاوز أي قيمة في المثيل الذي تم إنشاؤه حديثًا
- قم بتمرير قيم إضافية إلى خيار وظيفة البناء
دعونا نرى مثالا.
const factory = require('factory-girl').factory; const User = require('../models/user'); factory.define('user', User, (buildOptions) => { return { name: 'Mike', surname: 'Dow', email: buildOptions.email || '[email protected]' } }); const user1 = factory.build('user'); // {"name": "Mike", "surname": "Dow", "email": "[email protected]"} const user2 = factory.build('user', {name: 'John'}, {email: '[email protected]'}); // {"name": "John", "surname": "Dow", "email": "[email protected]"}
الاتصال بواجهات برمجة التطبيقات
بدء تشغيل خادم HTTP كامل وتقديم طلب HTTP فعلي ، فقط لتمزيقه بعد بضع ثوانٍ - خاصةً عند إجراء اختبارات متعددة - غير فعال تمامًا وقد يتسبب في أن تستغرق اختبارات التكامل وقتًا أطول بكثير من اللازم.
SuperTest هي مكتبة JavaScript لاستدعاء واجهات برمجة التطبيقات دون إنشاء خادم نشط جديد. يعتمد على SuperAgent ، مكتبة لإنشاء طلبات TCP. مع هذه المكتبة ، ليست هناك حاجة لإنشاء اتصالات TCP جديدة. يتم استدعاء واجهات برمجة التطبيقات على الفور تقريبًا.
SuperTest ، مع دعم للوعود ، هو اختبار supertest كما وعدت. عندما يعيد مثل هذا الطلب وعدًا ، فإنه يتيح لك تجنب العديد من وظائف رد النداء المتداخلة ، مما يجعل التعامل مع التدفق أسهل بكثير.
const express = require('express') const request = require('supertest-as-promised'); const app = express(); request(app).get("/recipes").then(res => assert(....));
تم إجراء اختبار SuperTest لإطار عمل Express.js ، ولكن مع التغييرات الصغيرة ، يمكن استخدامه مع أطر أخرى أيضًا.
المرافق الأخرى
في بعض الحالات ، هناك حاجة للسخرية من بعض التبعية في الكود الخاص بنا ، أو اختبار المنطق حول الوظائف باستخدام الجواسيس ، أو استخدام أبتر في أماكن معينة. هذا هو المكان الذي تكون فيه بعض حزم المرافق هذه مفيدة.
SinonJS هي مكتبة رائعة تدعم الجواسيس ، والمختبرين ، والسحاريين لإجراء الاختبارات. كما أنه يدعم ميزات اختبار مفيدة أخرى ، مثل وقت الانحناء ، واختبار وضع الحماية ، والتأكيد الموسع ، بالإضافة إلى الخوادم والطلبات المزيفة.
في بعض الحالات ، هناك حاجة للسخرية من بعض التبعية في التعليمات البرمجية الخاصة بنا. تستخدم الإشارات إلى الخدمات التي نود أن نسخر منها في أجزاء أخرى من النظام.
لحل هذه المشكلة ، يمكننا استخدام حقن التبعية ، أو إذا لم يكن ذلك خيارًا ، فيمكننا استخدام خدمة محاكاة ساخرة مثل Mockery.
تساعد الاستهزاء في الاستهزاء بالكود الذي يحتوي على تبعيات خارجية. لاستخدامه بشكل صحيح ، يجب استدعاء Mockery قبل تحميل الاختبارات أو الكود.
const mockery = require('mockery'); mockery.enable({ warnOnReplace: false, warnOnUnregistered: false }); const mockingStripe = require('lib/services/internal/stripe'); mockery.registerMock('lib/services/internal/stripe', mockingStripe);
باستخدام هذا المرجع الجديد (في هذا المثال ، mockingStripe
) ، من الأسهل الاستهزاء بالخدمات لاحقًا في اختباراتنا.
const stubStripeTransfer = sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null));
بمساعدة مكتبة Sinon ، من السهل السخرية. المشكلة الوحيدة هنا هي أن هذا كعب الروتين سينتشر في اختبارات أخرى. من أجل وضع الحماية ، يمكن استخدام صندوق رمل sinon. باستخدامه ، يمكن للاختبارات اللاحقة إعادة النظام إلى حالته الأولية.
const sandbox = require('sinon').sandbox.create(); const stubStripeTransfer = sandbox.sinon.stub(mockingStripe, 'transferAmount'); stubStripeTransfer.returns(Promise.resolve(null)); // after the test, or better when starting a new test sandbox.restore();
هناك حاجة لمكونات أخرى لوظائف مثل:
- تفريغ قاعدة البيانات (يمكن إجراؤه باستخدام استعلام هرمي واحد قبل الإنشاء)
- وضعه على حالة العمل (تركيبات تكميلية)
- الاستهزاء بطلبات TCP إلى خدمات الطرف الثالث (nock)
- استخدام تأكيدات أكثر ثراءً (شاي)
- الردود المحفوظة من جهات خارجية (إصلاح سهل)
اختبارات ليست بهذه البساطة
يعتبر التجريد والقابلية للتوسعة من العناصر الأساسية لبناء مجموعة اختبار تكامل فعالة. يجب تجميع كل ما يزيل التركيز من جوهر الاختبار (إعداد بياناته ، والعمل والتأكيد) وتلخيصه في وظائف المنفعة.
على الرغم من عدم وجود مسار صحيح أو خاطئ هنا ، نظرًا لأن كل شيء يعتمد على المشروع واحتياجاته ، لا تزال بعض الصفات الرئيسية شائعة في أي مجموعة اختبار تكامل جيدة.
يوضح الكود التالي كيفية اختبار API الذي ينشئ وصفة ويرسل بريدًا إلكترونيًا كأثر جانبي.
يقوم بإيقاف مزود البريد الإلكتروني الخارجي بحيث يمكنك اختبار ما إذا كان قد تم إرسال بريد إلكتروني دون إرسال بريد إلكتروني بالفعل. يتحقق الاختبار أيضًا مما إذا كانت واجهة برمجة التطبيقات قد استجابت برمز الحالة المناسب.
const co = require('co'); const factory = require('factory'); const superTest = require('../utils/super_test'); const basicEnv = require('../utils/basic_test_enivornment'); const path = '/v1/admin/recipes'; basicEnv.test(`API POST ${path}`, co.wrap(function* (t, assert, sandbox) { const chef = yield factory.create('chef'); const body = { chef_id: chef.id, recipe_name: 'cake', Ingredients: ['carrot', 'chocolate', 'biscuit'] }; const stub = sandbox.stub(mockery.emailProvider, 'sendNewEmail').returnsPromise(null); const serverResponse = yield superTest.get(path, body); assert.spies(stub).called(1); assert.statusCode(serverResponse, 201); }));
الاختبار أعلاه قابل للتكرار لأنه يبدأ ببيئة نظيفة في كل مرة.
لديها عملية إعداد بسيطة ، حيث يتم دمج كل ما يتعلق بالإعداد داخل وظيفة basicEnv.test
.
يختبر إجراء واحد فقط - API واحد. وهي تنص بوضوح على توقعات الاختبار من خلال عبارات التأكيد البسيطة. أيضًا ، لا يشتمل الاختبار على كود تابع لجهة خارجية عن طريق الاستهزاء / الاستهزاء.
ابدأ بكتابة اختبارات التكامل
عند دفع رمز جديد إلى الإنتاج ، يرغب المطورون (وجميع المشاركين الآخرين في المشروع) في التأكد من أن الميزات الجديدة ستعمل وأن الميزات القديمة لن تنكسر.
من الصعب جدًا تحقيق ذلك بدون اختبار ، وإذا تم القيام به بشكل سيئ يمكن أن يؤدي إلى الإحباط ، والتعب من المشروع ، وفي النهاية فشل المشروع.
تعتبر اختبارات التكامل ، جنبًا إلى جنب مع اختبارات الوحدة ، خط الدفاع الأول.
استخدام واحد فقط من الاثنين غير كافٍ وسيترك مساحة كبيرة للأخطاء المكتشفة. سيؤدي استخدام كليهما دائمًا إلى جعل الالتزامات الجديدة قوية ، ويوفر الثقة وإلهام الثقة في جميع المشاركين في المشروع.