في اليوم الآخر ، سأقدم تقريرًا داخليًا أخبر فيه مطورينا بالأخطاء غير السارة التي يمكن أن تحدث عند كتابة اختبارات الوحدة. إن أكثر الأخطاء غير السارة من وجهة نظري هي عندما تمر الاختبارات ، ولكن في نفس الوقت يفعلون ذلك بطريقة غير صحيحة بحيث يكون من الأفضل عدم النجاح. وقررت مشاركة أمثلة على مثل هذه الأخطاء مع الجميع. بالتأكيد شيء آخر لنقول من هذا المجال. تتم كتابة أمثلة على Node.JS و Mocha ، ولكن بصفة عامة هذه الأخطاء صحيحة لأي نظام بيئي آخر.
لجعلها أكثر إثارة للاهتمام ، يتم تأطير بعضها في شكل رمز المشكلة ومفسد ، وفتح ، والتي سوف ترى ما هي المشكلة. لذلك أوصي بأن تنظر أولاً إلى الكود ، وتجد خطأ فيه ، ثم تفتح المفسد. لن يتم توضيح أي حل للمشاكل - أقترح التفكير في الأمر بأنفسنا. فقط لأنني كسول. ترتيب القائمة ليس له معنى عميق - إنه مجرد تسلسل تذكرت فيه بكل أنواع المشاكل الحقيقية التي أدت بنا إلى دموع دموية. من المؤكد أن العديد من الأشياء ستبدو واضحة لك - ولكن حتى المطورين ذوي الخبرة يمكنهم كتابة مثل هذا الرمز عن طريق الخطأ.
لذلك دعونا نذهب.
0. عدم وجود اختبارات
الغريب ، ما زال الكثيرون يعتقدون أن اختبارات الكتابة تبطئ سرعة التطوير. بالطبع ، من الواضح أنه يجب قضاء المزيد من الوقت في كتابة الاختبارات وكتابة التعليمات البرمجية التي يمكن اختبارها. ولكن بعد تصحيح الأخطاء والانحدار بعد ذلك ، عليك قضاء عدة مرات المزيد من الوقت ...
1. عدم وجود اختبارات الجري
إذا كانت لديك اختبارات لا تعملها ، أو تعمل من وقت لآخر ، فهذا يشبه عدم وجود اختبارات. والأسوأ من ذلك - لديك رمز اختبار قديم وشعور زائف بالأمان. يجب أن تعمل الاختبارات على الأقل في عمليات CI عند دفع الكود إلى الفرع. والأفضل - محليا قبل الدفع. عندها لن يضطر المطور إلى العودة إلى البنية خلال بضعة أيام ، والتي ، كما اتضح ، لم تمر.
2. عدم وجود تغطية
إذا كنت لا تزال لا تعرف التغطية في الاختبارات ، فقد حان الوقت للذهاب والقراءة الآن. على الأقل
ويكيبيديا . وإلا ، فهناك فرصة جيدة أن يتحقق الاختبار من 10٪ من الشفرة التي تعتقد أنها تتحقق منها. عاجلاً أم آجلاً سوف تخطو بالتأكيد. بطبيعة الحال ، حتى تغطية 100 ٪ من الشفرة لا تضمن صحتها الكاملة بأي شكل من الأشكال - ولكن هذا أفضل بكثير من عدم وجود تغطية لأنها سوف تظهر لك المزيد من الأخطاء المحتملة. لا عجب أن أحدث إصدارات Node.JS لديها أدوات مدمجة لقراءتها. بشكل عام ، موضوع التغطية عميق وشامل للغاية ، لكنني لم أتعمق فيه - أريد أن أقول الكثير عن ذلك.
3.
const {assert} = require('chai'); const Promise = require('bluebird'); const sinon = require('sinon'); class MightyLibrary { static someLongFunction() { return Promise.resolve(1);
ما هو الخطأ هنامهلات في اختبارات الوحدة.
هنا أرادوا التحقق من أن تحديد مهلات الإعداد لعملية طويلة يعمل حقًا. بشكل عام ، لا يكون هذا منطقيًا على أي حال - لا يجب عليك التحقق من المكتبات القياسية - ولكن هذا الرمز يؤدي أيضًا إلى مشكلة أخرى - لزيادة وقت تنفيذ الاختبارات لفترة ثانية. يبدو أن هذا ليس كثيرًا ... لكن اضرب هذا في المرتبة الثانية بعدد الاختبارات المماثلة ، وعدد المطورين ، وعدد مرات الإطلاق يوميًا ... وستفهم أنه بسبب هذه المهلات ، يمكنك أن تفقد ساعات عديدة من العمل أسبوعيًا ، إن لم يكن يوميًا.
4.
const fs = require('fs'); const testData = JSON.parse(fs.readFileSync('./testData.json', 'utf8')); describe('some block', ()=>{ it('should do something', ()=>{ someTest(testData); }) })
ما هو الخطأ هناتحميل بيانات الاختبار خارج كتل الاختبار.
للوهلة الأولى ، يبدو أنه لا يهم مكان قراءة بيانات الاختبار - في الوصف أو حظرها أو في الوحدة النمطية نفسها. في الثانية أيضا. لكن تخيل أن لديك مئات الاختبارات ، والكثير منها يستخدم بيانات ثقيلة. إذا قمت بتحميلها خارج الاختبار ، فسيؤدي ذلك إلى حقيقة أن جميع بيانات الاختبار ستبقى في الذاكرة حتى نهاية تنفيذ الاختبار ، وبمرور الوقت ، سيستهلك الإطلاق المزيد والمزيد من ذاكرة الوصول العشوائي - حتى يتبين أن الاختبارات لم تعد تعمل على الإطلاق آلات العمل القياسية.
5.
const {assert} = require('chai'); const sinon = require('sinon'); class Dog {
ما هو الخطأ هنايتم استبدال رمز فعلا بذرة.
بالتأكيد رأيت على الفور هذا الخطأ السخيف. في الكود الحقيقي ، هذا ، بالطبع ، ليس واضحًا تمامًا - لكنني رأيت رمزًا معلقًا بذئير لدرجة أنني لم أختبر أي شيء على الإطلاق.
6.
const sinon = require('sinon'); const {assert} = require('chai'); class Widget { fetch() {} loadData() { this.fetch(); } } if (!sinon.sandbox || !sinon.sandbox.stub) { sinon.sandbox = sinon.createSandbox(); } describe('My widget', () => { it('is awesome', () => { const widget = new Widget(); widget.fetch = sinon.sandbox.stub().returns({ one: 1, two: 2 }); widget.loadData(); assert.isTrue(widget.fetch.called); }); });
ما هو الخطأ هناالاعتماد بين الاختبارات.
للوهلة الأولى ، من الواضح أنهم نسوا الكتابة هنا
afterEach(() => { sinon.sandbox.restore(); });
لكن المشكلة ليست فقط هذا ، ولكن يتم استخدام نفس الصندوق للرمل لجميع الاختبارات. ومن السهل جدًا الخلط بين بيئة تنفيذ الاختبار بطريقة تجعلهم يعتمدون على بعضهم البعض. بعد ذلك ، ستبدأ الاختبارات في ترتيب معين فقط ، وبشكل عام ليس من الواضح ما الذي يجب اختباره.
لحسن الحظ ، في مرحلة ما تم الإعلان عن إهمال sinon.sandbox وقصه ، بحيث يمكنك فقط مواجهة مثل هذه المشكلة في مشروع قديم - ولكن هناك العديد من الطرق الأخرى للتشويش على بيئة تنفيذ الاختبار بطريقة تجعل من المؤلم التحقيق فيها لاحقًا. الرمز الذي هو مذنب من السلوك غير الصحيح. بالمناسبة ، كان هناك مؤخرًا منشور على محور حول نوع من القوالب مثل "Ice Factory" - هذا ليس حلاً سحريًا ، لكنه يساعد في بعض الأحيان في مثل هذه الحالات.
7. بيانات اختبار ضخمة في ملف الاختبار
في كثير من الأحيان رأيت كيف أن ملفات JSON الضخمة ، وحتى XML ، تكمن مباشرة في الاختبار. أعتقد أنه من الواضح لماذا لا يستحق هذا الأمر - يصبح الأمر مؤلمًا للمشاهدة والتحرير ولن يشكرك أي IDE على ذلك. إذا كان لديك بيانات اختبار كبيرة ، أخرجها من ملف الاختبار.
8.
const {assert} = require('chai'); const crypto = require('crypto'); describe('extraTests', ()=>{ it('should generate unique bytes', ()=>{ const arr = []; for (let i = 0; i < 1000; i++) { const value = crypto.randomBytes(256); arr.push(value); } const unique = arr.filter((el, index)=>arr.indexOf(el) === index); assert.equal(arr.length, unique.length, 'Data is not random enough!'); }); });
ما هو الخطأ هنااختبارات اضافية.
في هذه الحالة ، كان المطور قلقًا للغاية من أن معرفاته الفريدة ستكون فريدة من نوعها ، لذلك كتب شيكًا لذلك. بشكل عام ، رغبة مفهومة - لكن من الأفضل قراءة الوثائق أو إجراء هذا الاختبار عدة مرات دون إضافته إلى المشروع. تشغيله في كل بناء لا معنى له.
حسنًا ، إن ربط القيم العشوائية في الاختبار هو في حد ذاته طريقة رائعة لإطلاق النار على قدمك عن طريق إجراء اختبار غير مستقر من نقطة الصفر.
9. عدم وجود موك
من الأسهل بكثير إجراء الاختبارات باستخدام قاعدة بيانات مباشرة وخدمات بنسبة 100 في المائة وتشغيل الاختبارات عليها.
ولكن عاجلاً أم آجلاً ، سيعود هذا المنتج إلى حيز التنفيذ - سيتم تنفيذ اختبارات إزالة البيانات على قاعدة المنتج ، أو ستبدأ في الانخفاض بسبب انقطاع خدمة الشريك ، أو ببساطة لن يكون لدى CI قاعدة لتشغيلها. بشكل عام ، يكون العنصر كليًا ، ولكن كقاعدة عامة - إذا كان يمكنك محاكاة الخدمات الخارجية ، فمن الأفضل القيام بذلك.
11.
const {assert} = require('chai'); class CustomError extends Error { } function mytestFunction() { throw new CustomError('important message'); } describe('badCompare', ()=>{ it('should throw only my custom errors', ()=>{ let errorHappened = false; try { mytestFunction(); } catch (err) { errorHappened = true; assert.isTrue(err instanceof CustomError); } assert.isTrue(errorHappened); }); });
ما هو الخطأ هناتصحيح الأخطاء المعقدة.
كل شيء ليس سيئًا ، ولكن هناك مشكلة واحدة - إذا تعطل الاختبار فجأة ، فسترى خطأ في النموذج
1) badCompare
should throw only my custom errors:
AssertionError: expected false to be true
+ expected - actual
-false
+true
at Context.it (test/011_badCompare/test.js:23:14)
علاوة على ذلك ، لفهم نوع الخطأ الذي حدث بالفعل - عليك إعادة كتابة الاختبار. لذلك في حالة حدوث خطأ غير متوقع - حاول أن يكون الاختبار يتحدث عن ذلك ، وليس فقط حقيقة حدوثه.
12.
const {assert} = require('chai'); function someVeryBigFunc1() { return 1;
ما هو الخطأ هناكل شيء في كتلة قبل.
يبدو أن هناك طريقة رائعة تتمثل في القيام بجميع العمليات في كتلة "قبل" ، وبالتالي ترك عمليات التحقق فقط داخلها.
ليس حقا
لأنه في هذه الحالة ، هناك فوضى لا يمكنك خلالها فهم وقت التنفيذ الفعلي للاختبارات ، ولا سبب السقوط ، ولا ما يتعلق باختبار ما ، وما الذي يحدث باختبار آخر.
لذلك يجب إجراء جميع أعمال الاختبار (باستثناء التهيئة القياسية) داخل الاختبار نفسه.
13.
const {assert} = require('chai'); const moment = require('moment'); function someDateBasedFunction(date) { if (moment().isAfter(date)) { return 0; } return 1; } describe('useFutureDate', ()=>{ it('should return 0 for passed date', ()=>{ const pastDate = moment('2010-01-01'); assert.equal(someDateBasedFunction(pastDate), 0); }); it('should return 1 for future date', ()=>{ const itWillAlwaysBeInFuture = moment('2030-01-01'); assert.equal(someDateBasedFunction(itWillAlwaysBeInFuture), 1); }); });
ما هو الخطأ هناالتعادل في التواريخ.
قد يبدو هذا أيضًا خطأً واضحًا - ولكنه يظهر أيضًا بشكل دوري بين المطورين المتعبين الذين يعتقدون بالفعل أن الغد لن يأتي أبدًا. والبناء الذي كان على ما يرام أمس يسقط فجأة اليوم.
تذكر أن أي تاريخ سيأتي عاجلاً أم آجلاً - لذلك إما استخدم مضاهاة الوقت بأشياء مثل `sinon.fakeTimers` ، أو على الأقل تعيين تواريخ بعيدة مثل 2050 - دع أحفادك يتأذون ...
14.
describe('dynamicRequires', ()=>{ it('should return english locale', ()=>{
ما هو الخطأ هناالتحميل الديناميكي للوحدات.
إذا كان لديك Eslint ، فربما تكون قد حظرت بالفعل التبعيات الديناميكية. ام لا
غالبًا ما أرى أن المطورين يحاولون تحميل مكتبات أو وحدات نمطية مختلفة مباشرةً داخل الاختبارات. ومع ذلك ، فإنهم يعرفون عمومًا كيف تعمل "المتطلبات" - لكنهم يفضلون الوهم بأنه من المفترض أن يتم إعطاؤهم نموذجًا نظيفًا لم يربك أحد حتى الآن.
هذا الافتراض خطير من حيث أن تحميل وحدات إضافية أثناء الاختبارات يكون أبطأ ، ويؤدي مرة أخرى إلى سلوك أكثر غير محدد.
15.
function someComplexFunc() {
ما هو الخطأ هناأسماء اختبار غير مفهومة.
يجب أن تكون متعبا من الأشياء الواضحة ، أليس كذلك؟ لكن لا يزال يتعين عليك أن تتحدث عن ذلك لأن الكثيرين لا يكلفون أنفسهم عناء كتابة أسماء مفهومة للاختبارات - ونتيجة لذلك ، لا يمكنك فهم ما يقوم به اختبار معين إلا بعد إجراء الكثير من الأبحاث.
16.
const {assert} = require('chai'); const Promise = require('bluebird'); function someTomeoutingFunction() { throw new Promise.TimeoutError(); } describe('no Error check', ()=>{ it('should throw error', async ()=>{ let timedOut = false; try { await someTomeoutingFunction(); } catch (err) { timedOut = true; } assert.equal(timedOut, true); }); });
ما هو الخطأ هناعدم التحقق من الخطأ القيت.
غالبًا ما تحتاج إلى التحقق من أن الوظيفة تلقي خطأً في بعض الحالات. ولكن عليك دائمًا التحقق مما إذا كانت هذه هي الروبوتات التي نبحث عنها - لأنه قد يتحول فجأة إلى خطأ آخر تم إلقاؤه في مكان آخر ولأسباب أخرى ...
17.
function someBadFunc() { throw new Error('I am just wrong!'); } describe.skip('skipped test', ()=>{ it('should be fine', ()=>{ someBadFunc(); }); });
ما هو الخطأ هنااختبارات المعوقين.
بالطبع ، قد ينشأ الموقف دائمًا عندما يكون الكود قد تم اختباره بالفعل عدة مرات بيدك ، وتحتاج إلى لفه بشكل عاجل ، ولسبب ما ، لا يعمل الاختبار. على سبيل المثال ، بسبب المضاعفات غير الواضحة لاختبار آخر ، والتي كتبت عنها سابقًا. ويتم تشغيل الاختبار. وهذا طبيعي. غير طبيعي - لا تقم بتعيين مهمة إعادة الاختبار على الفور. إذا لم يتم ذلك ، فسيتضاعف عدد الاختبارات المعطلة ، وسيصبح رمزها قديمًا. حتى يظل الخيار الوحيد - أظهر الرحمة ورمي كل هذه الاختبارات nafig ، لأنه من الأسرع كتابتها مرة أخرى بدلاً من فهم الأخطاء.
هنا جاء مثل هذا الاختيار. كل هذه الاختبارات تجتاز الاختبارات جيدًا ، لكنها مكسورة حسب التصميم. أضف خياراتك في التعليقات ، أو في
المستودع الذي أدليت به لجمع مثل هذه الأخطاء.