أفضل تقنيات الاختبار في JavaScript و Node.js


هذا دليل شامل لتوفير الموثوقية في JavaScript و Node.js. هنا يتم جمع العشرات من أفضل الوظائف والكتب والأدوات.

أولاً ، تعامل مع طرق الاختبار المقبولة عمومًا والتي تقوم عليها أي تطبيق. وبعد ذلك يمكنك الخوض في المجال الذي تهمك: الواجهة الأمامية والواجهات ، الواجهة الخلفية ، CI أو كل ما سبق.

محتوى



القسم 0. القاعدة الذهبية


0. القاعدة الذهبية: التمسك اختبار العجاف


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

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

الاختبارات - هذه فرصة للحصول على مساعد ودود ومبتسم ، من الجيد للغاية معه العمل والذي يعطي عائدًا كبيرًا على الاستثمارات الصغيرة. يعتقد العلماء أنه يوجد في دماغنا نظامان: نظام للأفعال التي لا تتطلب مجهودًا ، مثل القيادة على طريق فارغ ، والثاني للعمليات المعقدة التي تتطلب الوعي ، مثل حل المعادلات الرياضية. قم بإنشاء اختباراتك للنظام الأول ، بحيث عندما تنظر إلى الشفرة ، ستشعر بالبساطة مقارنة بتحرير مستند HTML ، وليس باستخدام حل 2X(17 × 24) .

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



معظم التوصيات الواردة أدناه مستمدة من هذا المبدأ.
هل انت جاهز

القسم 1. تشريح الاختبار


1.1 يجب أن يتكون اسم كل اختبار من ثلاثة أجزاء


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

  1. ما الذي يتم اختباره بالضبط؟ على سبيل المثال ، أسلوب ProductsService.addNewProduct .
  2. تحت أي ظروف وسيناريوهات؟ على سبيل المثال ، لا يتم تمرير السعر إلى الطريقة.
  3. ما هي النتيجة المتوقعة؟ على سبيل المثال ، لم يتم اعتماد منتج جديد.

على خلاف ذلك. فشل النشر ، فشل الاختبار المسمى "إضافة منتج". هل تفهم بالضبط ما الخطأ الذي يعمل؟

المذكرة. يحتوي كل فصل على رمز مثال وأحيانًا شكل توضيحي. انظر المفسدين.

أمثلة التعليمات البرمجية
كيف نفعل ذلك بشكل صحيح. يتكون اسم الاختبار من ثلاثة أجزاء.

 //1. unit under test describe('Products Service', function() { describe('Add new product', function() { //2. scenario and 3. expectation it('When no price is specified, then the product status is pending approval', ()=> { const newProduct = new ProductService().add(...); expect(newProduct.status).to.equal('pendingApproval'); }); }); }); 


1.2 هيكلة الاختبارات وفقا لنمط AAA


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

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

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

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

أمثلة التعليمات البرمجية
كيف نفعل ذلك بشكل صحيح. اختبار منظم وفقا لنمط AAA.

 describe.skip('Customer classifier', () => { test('When customer spent more than 500$, should be classified as premium', () => { //Arrange const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); //Act const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); //Assert expect(receivedClassification).toMatch('premium'); }); }); 

مثال على antipattern. لا فصل ، في قطعة واحدة ، أكثر صعوبة في التفسير.

 test('Should be classified as premium', () => { const customerToClassify = {spent:505, joined: new Date(), id:1} const DBStub = sinon.stub(dataAccess, "getCustomer") .reply({id:1, classification: 'regular'}); const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); expect(receivedClassification).toMatch('premium'); }); 


1.3 وصف التوقعات بلغة المنتج: اذكر نمط BDD


ما يجب القيام به. تسمح اختبارات البرمجة بأسلوب تعريفي للمستخدم بفهم الجوهر على الفور دون قضاء دورة واحدة من معالج الدماغ. عندما تكتب رمزًا إلزاميًا معبأًا بالمنطق الشرطي ، يتعين على القارئ بذل الكثير من الجهد. من وجهة النظر هذه ، تحتاج إلى وصف التوقعات بلغة شبيهة بالإنسان بأسلوب BDD التعريفي باستخدام توقع / ينبغي وعدم استخدام رمز مخصص. إذا لم يكن هناك تأكيد ضروري في Chai and Jest ، والذي يتكرر غالبًا ، فيمكنك توسيع Matcher Jest أو كتابة البرنامج المساعد الخاص بك لـ Chai .

على خلاف ذلك. سيقوم الفريق بكتابة عدد أقل من الاختبارات وتزيين الاختبارات المزعجة with .skip() .

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

مثال على antipattern. لفهم جوهر الاختبار ، يضطر المستخدم للحصول على رمز إلزامي طويل إلى حد ما.

 it("When asking for an admin, ensure only ordered admins in results" , ()={ //assuming we've added here two admins "admin1", "admin2" and "user1" const allAdmins = getUsers({adminOnly:true}); const admin1Found, adming2Found = false; allAdmins.forEach(aSingleUser => { if(aSingleUser === "user1"){ assert.notEqual(aSingleUser, "user1", "A user was found and not admin"); } if(aSingleUser==="admin1"){ admin1Found = true; } if(aSingleUser==="admin2"){ admin2Found = true; } }); if(!admin1Found || !admin2Found ){ throw new Error("Not all admins were returned"); } }); 

كيف نفعل ذلك بشكل صحيح. قراءة هذا الاختبار التعريفي واضحة.

 it("When asking for an admin, ensure only ordered admins in results" , ()={ //assuming we've added here two admins const allAdmins = getUsers({adminOnly:true}); expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"]) .but.not.include.ordered.members(["user1"]); }); 


1.4 الالتزام باختبار الصندوق الأسود: اختبار الطرق العامة فقط


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

على خلاف ذلك. سوف تتصرف الاختبارات الخاصة بك مثل صبي يهتف "الذئب!" : قم بالإبلاغ بصوت عالٍ عن الإيجابيات الخاطئة (على سبيل المثال ، فشل الاختبار بسبب تغيير اسم المتغير الخاص). ليس من المستغرب أن يبدأ الناس قريبًا في تجاهل إشعارات CI ، وفي يوم من الأيام سيفتقدون خطأ حقيقي ...

أمثلة التعليمات البرمجية
مثال على antipattern. اختبار الدواخل دون سبب وجيه.

مثال باستخدام موكا .

 class ProductService{ //this method is only used internally //Change this name will make the tests fail calculateVAT(priceWithoutVAT){ return {finalPrice: priceWithoutVAT * 1.2}; //Change the result format or key name above will make the tests fail } //public method getPrice(productId){ const desiredProduct= DB.getProduct(productId); finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice; } } it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => { //There's no requirement to allow users to calculate the VAT, only show the final price. Nevertheless we falsely insist here to test the class internals expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); }); 


1.5 اختيار التنفيذ المحاكى الصحيح: تجنب الأشياء المزيفة لصالح الرهانات والجواسيس


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

قبل استخدام تطبيقات المحاكاة ، اسأل نفسك أبسط سؤال: "هل يمكنني استخدام هذا لاختبار الوظائف التي ظهرت أو قد تظهر في الوثائق مع المتطلبات؟" إذا لم يكن كذلك ، فإنه ينم عن اختبار الصندوق الأبيض.

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

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

أمثلة التعليمات البرمجية
مثال على antipattern. الكائنات وهمية هي الشجاعة.

مثال باستخدام سينون .

 it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => { //Assume we already added a product const dataAccessMock = sinon.mock(DAL); //hmmm BAD: testing the internals is actually our main goal here, not just a side-effect dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false); new ProductService().deletePrice(theProductWeJustAdded); mock.verify(); }); 

كيف نفعل ذلك بشكل صحيح. تم تصميم الجواسيس لاختبار المتطلبات ، ولكن هناك تأثير جانبي - فهي تؤثر حتما على الدواخل.

 it("When a valid product is about to be deleted, ensure an email is sent", async () => { //Assume we already added here a product const spy = sinon.spy(Emailer.prototype, "sendEmail"); new ProductService().deletePrice(theProductWeJustAdded); //hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email) }); 


1.6 لا تستخدم "فو" ، استخدم مدخلات واقعية


ما يجب القيام به. غالبًا ما تحدث أخطاء الإنتاج مع بيانات إدخال محددة ومثيرة للدهشة. كلما كانت البيانات أكثر واقعية أثناء الاختبار ، زاد احتمال اصطياد الأخطاء في الوقت المحدد. لإنشاء بيانات حقيقية مزيفة تحاكي تنوع بيانات الإنتاج ونوعها ، استخدم مكتبات خاصة ، على سبيل المثال ، Faker . يمكن لهذه المكتبات إنشاء أرقام هواتف واقعية وأسماء مستعارة للمستخدمين وبطاقات مصرفية وأسماء شركات ، وحتى النص "lorem ipsum". يمكنك إنشاء اختبارات (أعلى اختبارات الوحدات ، وليس بدلاً منها) التي تقوم بتوزيع عشوائي للبيانات المزيفة لتناسب الوحدة في اختبار ، أو حتى استيراد بيانات حقيقية من بيئة الإنتاج. تريد أن تذهب أبعد من ذلك؟ اقرأ الفصل التالي (حول الاختبار القائم على الملكية).

على خلاف ذلك. سيبدو اختبار التطوير الخاص بك ناجحًا باستخدام المدخلات الاصطناعية مثل "Foo" ، وقد تتعطل بيانات الإنتاج عندما @3e2ddsf . ##' 1 fdsfds . fds432 AAAA المتطفلين خطًا صعبًا مثل @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA @3e2ddsf . ##' 1 fdsfds . fds432 AAAA .

أمثلة التعليمات البرمجية
مثال على antipattern. مجموعة اختبار تعمل بنجاح بسبب استخدام بيانات غير واقعية.

مثال باستخدام Jest .

 const addProduct = (name, price) =>{ const productNameRegexNoSpace = /^\S*$/;//no white-space allowed if(!productNameRegexNoSpace.test(name)) return false;//this path never reached due to dull input //some logic here return true; }; test("Wrong: When adding new product with valid properties, get successful confirmation", async () => { //The string "Foo" which is used in all tests never triggers a false result const addProductResult = addProduct("Foo", 5); expect(addProductResult).to.be.true; //Positive-false: the operation succeeded because we never tried with long //product name including spaces }); 

كيف نفعل ذلك بشكل صحيح. العشوائية مدخلات واقعية.

 it("Better: When adding new valid product, get successful confirmation", async () => { const addProductResult = addProduct(faker.commerce.productName(), faker.random.number()); //Generated random input: {'Sleek Cotton Computer', 85481} expect(addProductResult).to.be.true; //Test failed, the random input triggered some path we never planned for. //We discovered a bug early! }); 


1.7 استخدم الاختبار المستند إلى خاصية للتحقق من صحة مجموعات الإدخال المتعددة


ما يجب القيام به. عادة لكل اختبار نختار عدة عينات من بيانات الإدخال. حتى إذا كان تنسيق الإدخال مشابهًا للبيانات الحقيقية (انظر الفصل "لا تستخدم" foo ") ، فإننا لا نغطي سوى مجموعات قليلة من بيانات الإدخال (الطريقة ('', true, 1) ، الطريقة ("string" , false" , 0) ). ولكن في العملية ، يمكن استدعاء واجهة برمجة التطبيقات (API) التي تسمى بـ 5 معلمات بآلاف من المجموعات المختلفة ، يمكن أن تؤدي إحداها إلى تعطل العملية ( الدمج ). ماذا لو كان بإمكانك كتابة اختبار واحد يقوم تلقائيًا بإرسال 1000 مجموعة من بيانات الإدخال و تحديد ، في أي مجموعات رمز لا ترجع الإجابة الصحيحة؟ نفس الشيء نفعله مع م todike الاختبار على أساس الخصائص: عن طريق إرسال كل مزيج ممكن من إدخال البيانات في وحدة اختبار نزيد من فرصة للكشف عن الخلل، على سبيل المثال، لدينا أسلوب. addNewProduct(id, name, isDiscount) دعم مكتبته نسمي هذا الأسلوب مع عدد من المجموعات. (, , ) ، على سبيل المثال ، (1, "iPhone", false) ، (2, "Galaxy", true) ، وما إلى ذلك. يمكنك الاختبار بناءً على الخصائص باستخدام عداء الاختبار المفضل لديك (Mocha، Jest وما إلى ذلك) والمكتبات مثل js- check أو testcheck (لديها وثائق أفضل بكثير). يمكنك أيضًا تجربة المكتبة السريعة الفحص ، والتي توفر ميزات إضافية ويرافقها الكاتب بفعالية.

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

أمثلة التعليمات البرمجية
كيف نفعل ذلك بشكل صحيح. اختبار العديد من المجموعات مع المختبر testcheck.

 require('mocha-testcheck').install(); const {expect} = require('chai'); const faker = require('faker'); describe('Product service', () => { describe('Adding new', () => { //this will run 100 times with different random properties check.it('Add new product with random yet valid properties, always successful', gen.int, gen.string, (id, name) => { expect(addNewProduct(id, name).status).to.equal('approved'); }); }) }); 


1.8 إذا لزم الأمر ، استخدم اللقطات القصيرة والمضمنة فقط.


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

من ناحية أخرى ، تستفزنا أدلة وأدوات "اللقطة الكلاسيكية" لتخزين الملفات الكبيرة (على سبيل المثال ، الترميز لتقديم المكونات أو نتائج JSON API) على الوسائط الخارجية ومقارنة النتائج بالإصدار المحفوظ في كل مرة تقوم فيها بإجراء الاختبار. يمكن ، على سبيل المثال ، ربط اختبارنا ضمنيًا بـ 1000 سطر يحتوي على 3000 قيمة لم يراها مؤلف الاختبار مطلقًا ولم يكن يتوقعها. لماذا هذا سيء؟ لأن هناك 1000 سبب للاختبار. حتى سطر واحد يمكن أن يبطل لقطة ، وهذا يمكن أن يحدث في كثير من الأحيان. كم بعد كل مساحة أو تعليق أو تغيير بسيط في CSS أو HTML. بالإضافة إلى ذلك ، لن يخبرك اسم الاختبار بالفشل ، لأنه يتحقق فقط من أن 1000 سطر لم يتغير ، كما يشجع مؤلف الاختبار على أن يأخذ ما دامت وثيقة طويلة لا يستطيع تحليلها والتحقق منها. كل هذه الأعراض عبارة عن اختبار غامض ومتسرع ليس له مهمة واضحة ويحاول تحقيق الكثير.

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

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

أمثلة التعليمات البرمجية
مثال على antipattern. ربط اختبار مع بعض الأسطر 2000 غير المعروفة من التعليمات البرمجية.

 it('TestJavaScript.com is renderd correctly', () => { //Arrange //Act const receivedPage = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert expect(receivedPage).toMatchSnapshot(); //We now implicitly maintain a 2000 lines long document //every additional line break or comment - will break this test }); 

كيف نفعل ذلك بشكل صحيح. التوقعات واضحة وفي دائرة الضوء.

 it('When visiting TestJavaScript.com home page, a menu is displayed', () => { //Arrange //Act receivedPage tree = renderer .create( <DisplayPage page = "http://www.testjavascript.com" > Test JavaScript < /DisplayPage>) .toJSON(); //Assert const menu = receivedPage.content.menu; expect(menu).toMatchInlineSnapshot(` <ul> <li>Home</li> <li> About </li> <li> Contact </li> </ul> `); }); 


1.9 تجنب مقاعد الاختبار العالمية والبيانات الأولية ، أضف البيانات إلى كل اختبار على حدة


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

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

أمثلة التعليمات البرمجية
مثال على antipattern. الاختبارات ليست مستقلة وتستخدم نوعًا من الخطاف العام للحصول على بيانات عمومية من قاعدة البيانات.

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToUpdate = await SiteService.getSiteByName("Portal"); const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); expect(updateNameResult).to.be(true); }); it("When querying by site name, get the right site", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

كيف نفعل ذلك بشكل صحيح. يمكنك البقاء داخل الاختبار ، كل اختبار يعمل فقط مع البيانات الخاصة به.

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


1.10 لا تصاب بالأخطاء ، لكن توقعها


. , - , try-catch-finally , . ( ), .

Chai: expect(method).to.throw ( Jest: expect(method).toThrow() ). , , . , , .

. (, CI-) , .

مثال على antipattern. حالة اختبار طويلة تحاول اكتشاف خطأ باستخدام try-catch.

 /it("When no product name, it throws error 400", async() => { let errorWeExceptFor = null; try { const result = await addNewProduct({name:'nest'});} catch (error) { expect(error.code).to.equal('InvalidInput'); errorWeExceptFor = error; } expect(errorWeExceptFor).not.to.be.null; //if this assertion fails, the tests results/reports will only show //that some value is null, there won't be a word about a missing Exception }); 

. , , , QA .

 it.only("When no product name, it throws error 400", async() => { expect(addNewProduct)).to.eventually.throw(AppError).with.property('code', "InvalidInput"); }); 


1.11


. :

  • smoke-,
  • IO-less,
  • , , ,
  • , pull request', .

, , , #cold #api #sanity. . , Mocha : mocha — grep 'sanity' .

. , , , , , , , .

. '#cold-test' (Cold=== , - , ).

 //this test is fast (no DB) and we're tagging it correspondingly //now the user/CI can run it frequently describe('Order service', function() { describe('Add new order #cold-test #sanity', function() { it('Scenario - no currency was supplied. Expectation - Use the default currency #sanity', function() { //code logic here }); }); }); 


1.12


. , Node.js . , Node.

TDD . , , , . -- , - . , , . , . , , , , (, ..).

. , .

2:


️2.1 :


. 10 , . . , 10 (, , ), , , ? ?

: 2019- , TDD , , , . , , , . IoT-, Kafka RabbitMQ, , - . , , ? (, , ), , - .

( ) , , (« API, , !» (consumer-driven contracts)). , : , , , .

: TDD - . TDD , . , .

. ROI, Fuzz, , 10 .

. Cindy Sridharan 'Testing Microservices — the sane way'



مثال:



2.2


. , . , . , , ? — . : TDD-, .

«», API, , (, , in-memory ), , , . , , « », .

. , , 20 %.

. Express API ( ).



2.3 , API


. , ( ). - , ! — , , . « -22 » : , , . (consumer-driven contracts) PACT : , … ! PACT — «», . PACT- — . , API CI, .

. — .

.



2.4


. , Express-. . , , , JS- {req,res}. , (, Sinon ) {req,res}, , . node-mock-http {req,res} . , , HTTP-, res- (. ).

. Express- === .

. , Express-.

 //the middleware we want to test const unitUnderTest = require('./middleware') const httpMocks = require('node-mocks-http'); //Jest syntax, equivalent to describe() & it() in Mocha test('A request without authentication header, should return http status 403', () => { const request = httpMocks.createRequest({ method: 'GET', url: '/user/42', headers: { authentication: '' } }); const response = httpMocks.createResponse(); unitUnderTest(request, response); expect(response.statusCode).toBe(403); }); 


2.5


. . CI- , . (, ), (, ), . Sonarqube (2600+ ) Code Climate (1500+ ). :: Keith Holliday

. , .

. CodeClimate, :



2.6 , Node


. , . ( ) . , - , ? ? , API 50 % ? , Netflix - ( Chaos Engineering ). : , . , Netflix, chaos monkey , , , , - ( Kubernetes kube-monkey , ). , . , , Node- , , v8 1,7 , UX , ? node-chaos (-), , Node.

. , production .

. Node-chaos , Node.js, .



2.7 ,


. ( 0), , , . , (seeds) ( « » ) . , (. « »), , . . , , (, ).

. , , , ? , , , .

. - .

 before(() => { //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework await DB.AddSeedDataFromJson('seed.json'); }); it("When updating site name, get successful confirmation", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToUpdate = await SiteService.getSiteByName("Portal"); const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); expect(updateNameResult).to.be(true); }); it("When querying by site name, get the right site", async () => { //I know that site name "portal" exists - I saw it in the seed files const siteToCheck = await SiteService.getSiteByName("Portal"); expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ }); 

. , .

 it("When updating site name, get successful confirmation", async () => { //test is adding a fresh new records and acting on the records only const siteUnderTest = await SiteService.addSite({ name: "siteForUpdateTest" }); const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); expect(updateNameResult).to.be(true); }); 


3:


3.1. UI


. , , , . , , ( HTML CSS) . , (, , , ), , , .

. 10 , 500 (100 = 1 ) - - .

. .

 test('When users-list is flagged to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ { id: 1, name: 'Yoni Goldberg', vip: false }, { id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Extract the data from the UI first const allRenderedUsers = getAllByTestId('user').map(uiElement => uiElement.textContent); const allRealVIPUsers = allUsers.filter((user) => user.vip).map((user) => user.name); expect(allRenderedUsers).toEqual(allRealVIPUsers); //compare data with data, no UI here }); 

. UI .
 test('When flagging to show only VIP, should display only VIP members', () => { // Arrange const allUsers = [ {id: 1, name: 'Yoni Goldberg', vip: false }, {id: 2, name: 'John Doe', vip: true } ]; // Act const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>); // Assert - Mix UI & data in assertion expect(getAllByTestId('user')).toEqual('[<li data-testid="user">John Doe</li>]'); }); 


3.2 HTML- ,


. HTML- , . , , CSS-. , 'test-id-submit-button'. . , , .

. , , . — , , Ajax . . , CSS 'thick-border' 'thin-border'

. , .

 // the markup code (part of React component) <b> <Badge pill className="fixed_badge" variant="dark"> <span data-testid="errorsLabel">{value}</span> <!-- note the attribute data-testid --> </Badge> </b> // this example is using react-testing-library test('Whenever no data is passed to metric, show 0 as default', () => { // Arrange const metricValue = undefined; // Act const { getByTestId } = render(<dashboardMetric value={undefined}/>); expect(getByTestId('errorsLabel')).text()).toBe("0"); }); 

. CSS-.

 <!-- the markup code (part of React component) --> <span id="metric" className="d-flex-column">{value}</span> <!-- what if the designer changes the classs? --> // this exammple is using enzyme test('Whenever no data is passed, error metric shows zero', () => { // ... expect(wrapper.find("[className='d-flex-column']").text()).toBe("0"); }); 


3.3


. , , . , , . , — - , (. « » ). (, ) , .

, : , . ( ) . , .

. , . ?

. .

 class Calendar extends React.Component { static defaultProps = {showFilters: false} render() { return ( <div> A filters panel with a button to hide/show filters <FiltersPanel showFilter={showFilters} title='Choose Filters'/> </div> ) } } //Examples use React & Enzyme test('Realistic approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = mount(<Calendar showFilters={false} />) // Act wrapper.find('button').simulate('click'); // Assert expect(wrapper.text().includes('Choose Filter')); // This is how the user will approach this element: by text }) 

. .

 test('Shallow/mocked approach: When clicked to show filters, filters are displayed', () => { // Arrange const wrapper = shallow(<Calendar showFilters={false} title='Choose Filter'/>) // Act wrapper.find('filtersPanel').instance().showFilters(); // Tap into the internals, bypass the UI and invoke a method. White-box approach // Assert expect(wrapper.find('Filter').props()).toEqual({title: 'Choose Filter'}); // what if we change the prop name or don't pass anything relevant? }) 


3.4 .


. (, ). (, setTimeOut ) , . (, Cypress cy.request('url') ), API, wait(expect(element)) @testing-library/DOM . , API, , . , , hurry-up the clock . — , , ( ). , , - npm- , , wait-for-expect .

. , . , . .

. E2E API (Cypress).

 // using Cypress cy.get('#show-products').click()// navigate cy.wait('@products')// wait for route to appear // this line will get executed only when the route is ready 

. , DOM- (@testing-library/dom).
 // @testing-library/dom test('movie title appears', async () => { // element is initially not present... // wait for appearance await wait(() => { expect(getByText('the lion king')).toBeInTheDocument() }) // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 

. .

 test('movie title appears', async () => { // element is initially not present... // custom wait logic (caution: simplistic, no timeout) const interval = setInterval(() => { const found = getByText('the lion king'); if(found){ clearInterval(interval); expect(getByText('the lion king')).toBeInTheDocument(); } }, 100); // wait for appearance and return the element const movie = await waitForElement(() => getByText('the lion king')) }) 


3.5.


. - , . , , . : pingdom , AWS CloudWatch gcp StackDriver , , SLA. , , (, lighthouse , pagespeed ), . — , : , (TTI) . , , , , , DOM, SSL . , CI, 247 CDN.

. , , , , - CDN.

. Lighthouse .



3.6 API


. ( 2), , , ( ). API (, Sinon , Test doubles ), API. . API , ( ). API, . , , API . , : .

. , API 100 , 20 .

. API-.

 // unit under test export default function ProductsList() { const [products, setProducts] = useState(false) const fetchProducts = async() => { const products = await axios.get('api/products') setProducts(products); } useEffect(() => { fetchProducts(); }, []); return products ? <div>{products}</div> : <div data-testid='no-products-message'>No products</div> } // test test('When no products exist, show the appropriate message', () => { // Arrange nock("api") .get(`/products`) .reply(404); // Act const {getByTestId} = render(<ProductsList/>); // Assert expect(getByTestId('no-products-message')).toBeTruthy(); }); 


3.7 ,


. E2E (end-to-end, ) UI (. 3.6). , , . , , - . , — (, ), . - , , UI- Cypress Pupeteer . , : 50 , , . 10 . , , , — . , .

. UI , , ( , UI) .

3.8


. , API , , . (before-all), - . , : . , . - API- . , . (, ), , , . , : , API (. 3.6).

. , 200 , 100 , 20 .

. (before-all), (before-each) (, Cypress).

Cypress .

 let authenticationToken; // happens before ALL tests run before(() => { cy.request('POST', 'http://localhost:3000/login', { username: Cypress.env('username'), password: Cypress.env('password'), }) .its('body') .then((responseFromLogin) => { authenticationToken = responseFromLogin.token; }) }) // happens before EACH test beforeEach(setUser => () { cy.visit('/home', { onBeforeLoad (win) { win.localStorage.setItem('token', JSON.stringify(authenticationToken)) }, }) }) 


3.9 smoke-,


. production- , , . , , , , . smoke- . production, , , . , smoke- , .

. , , production . /Payment.

. Smoke- .

 it('When doing smoke testing over all page, should load them all successfully', () => { // exemplified using Cypress but can be implemented easily // using any E2E suite cy.visit('https://mysite.com/home'); cy.contains('Home'); cy.contains('https://mysite.com/Login'); cy.contains('Login'); cy.contains('https://mysite.com/About'); cy.contains('About'); }) 


3.10


. , . «» , , , , . , ( ) , , -, , , . « », . . , Cucumber JavaScript . StoryBook UI- , (, , , ..) , . , , .

. , .

. cucumber-js.

 // this is how one can describe tests using cucumber: plain language that allows anyone to understand and collaborate Feature: Twitter new tweet I want to tweet something in Twitter @focus Scenario: Tweeting from the home page Given I open Twitter home Given I click on "New tweet" button Given I type "Hello followers!" in the textbox Given I click on "Submit" button Then I see message "Tweet saved" 

. Storybook , .



3.11


. , . , . , . , - . , . , , , . , - . UI « ». , (, wraith , PhantomCSS), . (, Applitools , Perci.io ) , , « » (, ), DOM/CSS, .

. , ( ) , ?

. : , .



. wraith UI.

 ​# Add as many domains as necessary. Key will act as a label​ domains: english: "http://www.mysite.com"​ ​# Type screen widths below, here are a couple of examples​ screen_widths: - 600​ - 768​ - 1024​ - 1280​ ​# Type page URL paths below, here are a couple of examples​ paths: about: path: /about selector: '.about'​ subscribe: selector: '.subscribe'​ path: /subscribe 


4:


4.1 (~80 %),


. — , . , . — (, ), . ? , 10-30 % . 100 % , . . , : Airbus, ; , 50 % . , , 80 % ( Fowler: «in the upper 80s or 90s» ), , , .

: (CI), ( Jest ) , . , . , ( ) — . , — , , . , .

. . , , . .

.



. ( Jest).



4.2 ,


. , . , , , , . , , - , . PricingCalculator , , , 10 000 … , , . , . 80- , . : , , , . , - .

. , , , .

. ? , QA . : , - . , - API .




4.3


. : 100 %, . كيف ذلك؟ , , , . . - : , , , .

, . JavaScript- Stryker :

  1. « ». , newOrder.price===0 newOrder.price!=0 . «» .
  2. , , : , . , , .

, , , .

. , 85- 85 % .

. 100 %, 0 %.

 function addNewOrder(newOrder) { logger.log(`Adding new order ${newOrder}`); DB.save(newOrder); Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`); return {approved: true}; } it("Test addNewOrder, don't use such test names", () => { addNewOrder({asignee: "John@mailer.com",price: 120}); });//Triggers 100% code coverage, but it doesn't check anything 

. Stryker reports, , ().



4.4 -


. ESLint. , eslint-plugin-mocha , ( describe() ), , . eslint-plugin-jest , ( ).

. 90- , , , . , .

. , , .

 describe("Too short description", () => { const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words }); it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite expect("somevalue"); // error:no-assert }); it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests }); 


5: CI


5.1 ,


. — . , . , ( !). , . ( ESLint standard Airbnb ), . , eslint-plugin-chai-expect , . Eslint-plugin-promise ( ). Eslint-plugin-security , DOS-. eslint-plugin-you-dont-need-lodash-underscore , , V8, , Lodash._map(…) .

. , , . ما الذي يحدث؟ , , . . , , .

. , . , ESLint production-.



5.2


. CI , , ..? , . لماذا؟ : (1) -> (2) -> (3) . , , .

, , , , - .

CI- ( , CircleCI local CLI ) . , wallaby , ( ) . npm- package.json, , (, , , ). (non-zero exit code) concurrently . — , npm run quality . githook ( husky ).

. , .

. Npm-, , , .

 "scripts": { "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"", "inspect:lint": "eslint .", "inspect:vulnerabilities": "npm audit", "inspect:license": "license-checker --failOn GPLv2", "inspect:complexity": "plato .", "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\"" }, "husky": { "hooks": { "precommit": "npm run inspect:all", "prepush": "npm run inspect:all" } } 


5.3 production-


. — CI-. . — Docker-compose . (, ) production-. AWS Local AWS-. , serverless AWS SAM Faas-.

Kubernetes CI-, . , « Kubernetes» Minikube MicroK8s , , . « Kubernetes»: CI- (, Codefresh ) Kubernetes-, CI- ; .

. .

: CI-, Kubernetes- (Dynamic-environments Kubernetes )

 deploy: stage: deploy image: registry.gitlab.com/gitlab-examples/kubernetes-deploy script: - ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN - kubectl create ns $NAMESPACE - kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL" - mkdir .generated - echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF" - sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml" - kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml - kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml environment: name: test-for-ci 


5.4


. , , . , 500 , , . , CI- ( Jest , AVA Mocha ) , . CI- (!), . , CLI , , .

. — , .

. Mocha parallel Jest Mocha ( JavaScript Test-Runners Benchmark )



5.5


. , . 10 ? CI- npm- license check plagiarism check ( ), , , Stackoveflow .

. , , .

.
 //install license-checker in your CI environment or also locally npm install -g license-checker //ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build license-checker --summary --failOn BSD 



5.6


. , Express, . npm audit , snyk ( ). CI .

. . .

: NPM Audit



5.7


. package-lock.json Yarn npm ( ): . npm install npm update , . , — . , package.json ncu .

, . :

  • CI , , npm outdated npm-check-updates (ncu). .
  • , pull request' .

: ? , ( , eslint-scope ). « »: latest , , (, 1.3.1, — 1.3.8).

. , .

: , , ncu CI-.



5.8 CI-, Node


. , Node . , Node.

  1. . , Jenkins .
  2. Docker.
  3. , . . smoke-, (, , ) .
  4. , , , , , .
  5. , . , -. - ( ).
  6. . .
  7. , , .
  8. (, Docker-).
  9. , . node_modules .

. , .

5.9 : CI-, Node


. , , . Node, CI . , MySQL, Postgres. CI- «», MySQl, Postgres Node. , - (, ). CI, , .

. - ?

: Travis ( CI) Node.

 language: node_js node_js: - "7" - "6" - "5" - "4" install: - npm install script: - npm run test 

Source: https://habr.com/ru/post/ar466879/


All Articles