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

الاختبار الآلي لشيفرات جافاسكربت باستخدام mocha


Emq Mohammed

يُستخدَم الاختبار الآلي في الكثير من المهام، كما يستخدم بكثرة في المشاريع الحقيقية.

لم نحتاج الاختبارات؟

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

إن كان هناك خطأ، فإننا نُصلِح الشيفرة البرمجية، ونُعيد تشغيلها، ونفحص النتائج. وهكذا حتى تصبح صحيحة. لكن هذه الطريقة «إعادة التشغيل» غير مثالية.

عند اختبار شيفرة برمجية عن طريق إعادة التشغيل اليدوية، فمن السهل نسيان شيئٍ ما.

على سبيل المثال، عند إنشاء الدالة f. نكتب فيها بعض الشيفرات البرمجية، ثم نَفحصها: "f(1)‎" تعمل لكن "f(2)‎" لا تعمل. صلِح الشيفرة حتى تعمل "f(2)‎". ثم تبدو الدالة كأنها مكتملة، لكننا ننسى إعادة اختبار "f(1)‎" مما قد يؤدي إلى خطأ.

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

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

التطوير المستند إلى السلوك

لنبدأ بتقنية تسمى التطوير المستند إلى السلوك Behavior Driven Development أو كاختصار BDD.

هذه التقنية BDD هي 3 في 1: اختبارات وتوثيق وامثلة.

سنجرب حالة تطوير عملية لفهم BDD.

تطوير الدالة "pow": الوصف

لنفترض أننا نريد إنشاء الدالة pow(x, n)‎ التي ترفع الأساس x إلى القوة n. مع الأخذ بالحسبان أن n≥0. هذه المهمة هي مجرد مثال: المعامل ** يقوم بهذه العملية في JavaScript، لكننا نركز هنا على تدفق التطوير الذي يمكن تطبيقه على مهام أكثر تعقيدا.

يمكننا تخيل ووصف ما يجب أن تقوم به الدالة pow قبل إنشاء شيفرتِها البرمجية. هذا الوصف يُسمى "specification" أو باختصار "spec" ويحتوي على وصف حالات الاستخدام بالإضافة إلى اختبارات لهذه الحالات كالتالي:

describe("pow", function() {

  it("raises to n-th power", function() {
    assert.equal(pow(2, 3), 8);
  });

});

تحتوي المواصفات على 3 أجزاء رئيسية كما ترى في الأعلى:

describe("title", function() { ... })

ماهي الوظيفة التي نصفها، في هذه الحالة، نحن نصف الدالة pow. تستخدم بواسطة العاملين- أجزاء it.

it("use case description", function() { ... })

نصف (نحن بطريقة مقروءة للبشر) حالة الاستخدام المخصصة في عنوان it، والمعامل الآخر عبارة عن دالة تفحص هذه الدالة.

assert.equal(value1, value2)

الشيفرة البرمجية بداخل it يجب أن تُنَفَّذ بدون أخطاء في حال كان التنفيذ صحيحًا.

تستخدم الدوال assert.* لِفحص ما إن كانت الدالة pow تعمل بالشكل المتوقع أم لا. تستخدم إحدى هذه الدوال في هذا المثال - assert.equal، والتي توازن معاملَين وتُرجِع خطأ في حال عدم تساويهما. في المثال تفحص هذه الدالة إن كانت نتيجة تنفيذ الدالة pow(2, 3)‎ تساوي 8. كما يوجد العديد من أنواع التحقق والمقارنة والتي سنُضيفها لاحقا.

يمكن تنفيذ الوصف، وسينفِّذ الفحص الموجود بداخل it كما سنرى لاحقا.

تدفق التطوير

يبدو تدفق التطوير غالبا كما يلي:

  1. يُكتب الوصف الأولي مع فحص للوظيفة الرئيسية.
  2. يُنشَئ تنفيذ أولي.
  3. للتأكد من صحة عمل التنفيذ، نُشَغِّل إطار التقييم Mocha الذي يُشَغِّل الوصف. ستظهر أخطاء في حال عدم اكتمال الوظائف. نُصحح الأخطاء حتى يصبح كل شيء صحيحًا.
  4. هكذا أصبح لدينا تنفيذ مبدئي يعمل كالمطلوب بالإضافة إلى فحصه.
  5. نضيف المزيد من حالات الاستخدام للوصف، ربما بعض هذه الميزات ليس مضمنا في التنفيذ بعد. حينها يبدأ الاختبار بالفشل.
  6. عُد للخطوة 3 وحدِّث التنفيذ إلى أن تختفي كل الأخطاء.
  7. كرر الخطوات 3-6 حتى تجهز كل الوظائف.

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

لنُجرب تدفق التطوير هذا على حالتنا العملية.

الخطوة 1 أصبحت جاهزة: لدينا وصفًا مبدئيًّا للدالة pow. الآن وقبل التنفيذ، لِنستخدم بعض مكاتب جافاسكربت لتشغيل الاختبار حتى نتأكد من إن كانت تعمل (لن تعمل).

المواصفات أثناء التنفيذ

سنستخدم في هذا الشرح مكاتب جافاسكربت التالية للاختبار:

  • Mocha - الإطار الرئيسي: يوفر دوال الفحص الأكثر استخدامًا ما يشمل describe و it بالإضافة إلى الدوال الرئيسية التي تُشَغِّل الاختبار.
  • Chai - المكتبة المحتوية على دوال تأكيدية. تتيح لنا استخدام العديد من هذه الدوال، نحتاج الآن assert.equal فقط.
  • Sinon - مكتبة للتجسس على الدوال، ومحاكاة الدوال المدمجة، والمزيد؛ سنحتاج هذه المكتبة لاحقا.

تُعد هذه المكاتب مفيدة للاختبار في كل من المتصفح والخادم. سنأخذ بعين الاعتبار هنا جهة المتصفح. صفحة HTML كاملة مع هذه المكاتب ووصف الدالة pow:

    <!DOCTYPE html>
    <html>
    <head>
      <!-- add mocha css, to show results -->
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css">
      <!-- add mocha framework code -->
      <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script>
      <script>
    mocha.setup('bdd'); // minimal setup
      </script>
      <!-- add chai -->
      <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script>
      <script>
    // chai has a lot of stuff, let's make assert global
    let assert = chai.assert;
      </script>
    </head>

    <body>

    <script>
    function pow(x, n) {
    /* function code is to be written, empty now */
    }
    </script>

    <!-- the script with tests (describe, it...) -->
    <script src="test.js"></script>

    <!-- the element with id="mocha" will contain test results -->
    <div id="mocha"></div>

    <!-- run tests! -->
    <script>
    mocha.run();
    </script>
    </body>

    </html>

يمكن تقسيم الصفحة إلى 5 أجزاء:

  1. <head> - لإضافة مكاتب وأنماط للاختبارات.
  2. <script> يحتوي على الدالة التي سيتم اختبارها، في هذا المثال الشيفرة البرمجية للدالة pow.
  3. الاختبار - عبارة عن سكريبت خارجي في هذا المثال test.js يحتوي على describe("pow", ...)‎ الموضح سابقا.
  4. عنصر HTML ‏<div id="mocha"‎> والذي سيُستخدم بواسطة Mocha لعرض النتائج.
  5. يتم بدء الاختبارات باستخدام الأمر mocha.run()‎.

النتائج:

يفشل الاختبار ويظهر خطأ في الوقت الحالي. يُعد هذا منطقيا: فالدالة pow ما زالت فارغة، فإن pow(2,3)‎ تُرجع undefined بدلا من 8.

لاحظ أن بإمكانك تشغيل أكثر من اختبار مختلف مستقبلا، فهناك مُشغلات عالية المستوى مثل karma وغيرها.

التنفيذ الأولي

لنقم بتنفيذ مبسط للدالة pow حتى تعمل الاختبارات:

function pow(x, n) {
  return 8; // :) we cheat!
}

الآن ستعمل…

تطوير الوصف

ما قمنا به هو غش فقط، لا تعمل الدالة كالمطلوب: إن قمنا بحساب pow(3,4)‎ فسنحصل على قيمة غير صحيحة، لكنها ستجتاز الاختبارات.

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

لنضف اختبارًا آخر للتأكد ما إن كان pow(3, 4) = 81.

يمكنُنا اختيار إحدى الطريقتين لتنظيم الاختبارات:

  1. الخيار الأول - إضافة assert إلى it:
describe("pow", function() {

  it("raises to n-th power", function() {
    assert.equal(pow(2, 3), 8);
    assert.equal(pow(3, 4), 81);
  });

});
  1. الخيار الآخر - عمل اختبارين:
describe("pow", function() {

  it("2 raised to power 3 is 8", function() {
    assert.equal(pow(2, 3), 8);
  });

  it("3 raised to power 3 is 27", function() {
    assert.equal(pow(3, 3), 27);
  });

});

يختلف المبدئ في أنه عند وجود خطأ في assert، فإن it تتوقف عن العمل. لذا ففي الخيار الأول عند فشل assert الأولى فلن نرى مخرجات assert الأخرى. يُعد الخيار الثاني أفضل للحصول على المزيد من المعلومات حول ما يحدث بعمل اختبارين منفصلين. بالإضافة إلى وجود قاعدة أخرى من الجيد اتباعها.

كل اختبار يفحص شيئًا واحدًا فقط.

إن وجدنا اختبارًا يفحص شيئين مختلفين فمن الأفضل فصلُهما إلى اختبارين. لنكمل باستخدام الخيار الثاني.

النتائج:

سيفشل الاختبار الثاني كَالمتوقع. فَالدالة تُرجع دوما 8، بينما تتوقع الدالة assert النتيجة 27.

تطوير التنفيذ

لنكتب شيئا أكثر واقعية لاجتياز الاختبارات:

function pow(x, n) {
  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}

للتأكد من صحة عمل الدالة K نختبرها لأكثر من قيمة. يمكننا القيام بذلك باستخدام الحلقة for بدلا من تكرار it يدويا:

describe("pow", function() {

  function makeTest(x) {
    let expected = x * x * x;
    it(`${x} in the power 3 is ${expected}`, function() {
      assert.equal(pow(x, 3), expected);
    });
  }

  for (let x = 1; x <= 5; x++) {
    makeTest(x);
  }

});

النتيجة:

دالة describe متداخلة

سنضيف المزيد من الاختبارات، لكن، قبل ذلك لاحظ أنه يجب جمع الدالة المساعدة makeTest والدالة for. لن نَحتاج لاستخدام makeTest في باقي الاختبارات، نحتاجها فقط في for: لأن وظيفتهُما العامة هي فحص كيف ترفع الدالة pow قيمة ما إلى قوة معينة.

يتم جمع الدالتين باستخدام الدالة describe المتداخلة:

describe("pow", function() {

  describe("raises x to power 3", function() {

    function makeTest(x) {
      let expected = x * x * x;
      it(`${x} in the power 3 is ${expected}`, function() {
        assert.equal(pow(x, 3), expected);
      });
    }

    for (let x = 1; x <= 5; x++) {
      makeTest(x);
    }

  });

  // ... more tests to follow here, both describe and it can be added
});

تُعرِّف describe الداخلية مجموعة فرعية جديدة من الاختبارات. يمكن ملاحظة الإزاحة في المخرجات:

يمكننا إضافة المزيد من دوال it و describe في الطبقة العلوية مع دوال مساعدة لها، هذه الدوال لن ترى الدالة makeTest.

before/after و beforeEach/afterEach

يمكننا إعداد دوال before/after التي تُنَفَّذ قبل/بعد تنفيذ الاختبارات، كما يمكننا استخدام beforeEach/afterEach قبل/بعد كل it.

على سبيل المثال:

describe("test", function() {

  before(() => alert("Testing started – before all tests"));
  after(() => alert("Testing finished – after all tests"));

  beforeEach(() => alert("Before a test – enter a test"));
  afterEach(() => alert("After a test – exit a test"));

  it('test 1', () => alert(1));
  it('test 2', () => alert(2));

});

تسلسل التنفيذ سيكون كالتالي:

Testing started  before all tests (before)
Before a test  enter a test (beforeEach)
1
After a test  exit a test   (afterEach)
Before a test  enter a test (beforeEach)
2
After a test  exit a test   (afterEach)
Testing finished  after all tests (after)

تستخدم beforeEach/afterEach و before/after غالبا لتنفيذ الخطوات الأولية، العدادات التي تُخرج 0 أو للقيام بشيء ما بين الاختبارات أو مجموعة اختبارات.

توسيع الوصف

أصبحت الوظيفة الرئيسية للدالة pow مكتملة. تم تجهيز الدورة الأولى من التطوير. الآن يمكننا الاحتفال بالانتهاء من ذلك وبدء تطوير الدالة.

كما ذكرنا مسبقا، فإن الدالة pow(x, n)‎ ستتعامل مع أرقام موجبة فقط n. تُرجع دوال جافاسكربت دائما NaN عند وجود خطأ حسابي. لنقم بهذا الأمر مع قيم n.

أولا، نضيف هذا إلى الوصف:

describe("pow", function() {

  // ...

  it("for negative n the result is NaN", function() {
    assert.isNaN(pow(2, -1));
  });

  it("for non-integer n the result is NaN", function() {
    assert.isNaN(pow(2, 1.5));
  });

});

النتائج مع الاختبارات الجديدة:

تفشل الاختبارات المُضافة مؤخرا وذلك لأن التنفيذ لا يدعمها. هكذا هي الطريقة التي يعمل بها BDD: نبدأ بكتابة الاختبارات التي نعلم بأنها ستفشل ثم نكتب التنفيذ الخاص بها.

دوال تأكيد أخرى

لاحظ أن الدالة assert.isNaN: تفحص وجود القيمة NaN.

يوجد المزيد من دوال التأكيد في Chai، مثلا:

  • assert.equal(value1, value2)‎ - تفحص التساوي value1 == value2.
  • assert.strictEqual(value1, value2)‎ - تفحص التساوي التام value1 === value2.
  • assert.notEqual، assert.notStrictEqual - تفحص عكس الدالتين أعلاه.
  • assert.isTrue(value)‎ - تفحص أن value === true.
  • assert.isFalse(value)‎ - تفحص أن value === false.
  • يمكنك قراءة باقي الدوال في التوثيق

لذا، يجب أن نضيف بعض الأسطر للدالة pow:

function pow(x, n) {
  if (n < 0) return NaN;
  if (Math.round(n) != n) return NaN;

  let result = 1;

  for (let i = 0; i < n; i++) {
    result *= x;
  }

  return result;
}

الآن أصبحت تعمل وتَجتاز جميع الاختبارات:

الخلاصة

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

يمكن استخدام الوصف بثلاث طرق:

  1. اختبارات - تضمن صحة عمل الشيفرة البرمجية.
  2. توثيق - توضح العناوين في describe وit وظيفة الدالة.
  3. أمثلة - تُعد الاختبارات أمثلة فعالة تعرض كيف يمكن استخدام الدالة.

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

يوجد خيارين بدون اختبارات:

  1. تنفيذ التغيير بغض النظر عن النتائج. هكذا قد نواجه الكثير من الأخطاء عند الفحص اليدوي.
  2. يتجنب المطورون تحديث الشيفرة عند توقع حدوث أخطاء فادحة وعدم وجود اختبارات لفحص هذه الأخطاء فتبقى الشيفرة البرمجية بدون تحديث.

يساعد الفحص الآلي على تجنب هذه المشاكل!

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

علاوة على ذلك، الشيفرة البرمجية المختبرة جيدا تكون بهيكل أفضل.

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

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

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

تمارين

ما الخطأ في الاختبار التالي؟

الأهمية: 5

ما الخطأ في الاختبار للدالة pow أدناه؟

it("Raises x to the power n", function() {
  let x = 5;

  let result = x;
  assert.equal(pow(x, 1), result);

  result *= x;
  assert.equal(pow(x, 2), result);

  result *= x;
  assert.equal(pow(x, 3), result);
});

الحل:

يوضح الاختبار أحد الإغراءات التي يواجهها المطور أثناء كتابة اختبار. ما لدينا الآن هو ثلاثة اختبارات، لكنها مرتبة كدالة واحدة تتضمن ثلاثة تأكيدات.

تكون هذه الطريقة أسهل في بعض الأحيان، لكن إن وُجِد أي خطأ، فلن يكون من السهل معرفة سبب ذلك الخطأ. إن حدث خطأ ما أثناء تنفيذ شيفرة معقدة، فَسنحتاج لمعرفة قيم البيانات عند تلك النقطة. أي أننا سنحتاج لتنقيح الاختبار.

من الأفضل تجزئة الاختبار إلى أجزاء it متعددة مع مدخلات ومخرجات محددة بوضوح كما يلي:

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  it("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});

بدلنا it مكان describe وبعدد من it. إن فشل شيء ما الآن فَسَنتمكن من رؤية البيانات. يمكننا أيضًا عزل اختبار واحد وتشغيلة في وضع مستقل باستخدام it.only بدلًا من it:

describe("Raises x to power n", function() {
  it("5 in the power of 1 equals 5", function() {
    assert.equal(pow(5, 1), 5);
  });

  // Mocha will run only this block
  it.only("5 in the power of 2 equals 25", function() {
    assert.equal(pow(5, 2), 25);
  });

  it("5 in the power of 3 equals 125", function() {
    assert.equal(pow(5, 3), 125);
  });
});

ترجمة -وبتصرف- للفصل Automated testing with mocha من كتاب The JavaScript Language

اقرأ أيضًا


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

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

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



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...