أدوات المهنة

الاختبارات الآلية هي في الأساس رموز برمجية تُطرح أو تتسبب في حدوث خطأ في حال حدوث خطأ. توفر معظم المكتبات أو أطر الاختبار مجموعة متنوعة من العناصر الأولية التي تجعل كتابة الاختبارات أسهل.

وكما ورد في القسم السابق، تتضمّن هذه المبادئ الأساسية دائمًا طريقة لتعريف الاختبارات المستقلة (يُشار إليها باسم حالات الاختبار) وتقديم تأكيدات. التأكيدات هي طريقة للجمع بين التحقق من النتيجة وطرح خطأ إذا كان هناك خطأ ما، ويمكن اعتبارها العامل الأساسي لكل مبادئ الاختبار الأساسية.

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

مثال:

import { fibonacci, catalan } from '../src/math.js';
import { assert, test, suite } from 'a-made-up-testing-library';

suite('math tests', () => {
  test('fibonacci function', () => {
    // check expected fibonacci numbers against our known actual values
    // with an explanation if the values don't match
    assert.equal(fibonacci(0), 0, 'Invalid 0th fibonacci result');
    assert.equal(fibonacci(13), 233, 'Invalid 13th fibonacci result');
  });
  test('relationship between sequences', () => {
    // catalan numbers are greater than fibonacci numbers (but not equal)
    assert.isAbove(catalan(4), fibonacci(4));
  });
  test('bugfix: check bug #4141', () => {
    assert.isFinite(fibonacci(0)); // fibonacci(0) was returning NaN
  })
});

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

أدوات مساعدة التأكيد كأساسيات

تشتمل معظم إطارات عمل الاختبار، بما في ذلك Vitest، على مجموعة من أدوات المساعدة في التأكيد على عنصر assert تتيح لك التحقّق بسرعة من القيم المعروضة أو الحالات الأخرى مقارنةً ببعض expectation. غالبًا ما يكون هذا التوقع قيمًا "جيدة معروفة". في المثال السابق، نعلم أن رقم فيبوناتشي الثالث عشر يجب أن يكون 233، لذا يمكننا تأكيد ذلك مباشرة باستخدام assert.equal.

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

  • إنّ علامة الاختيار "truthy"، التي يتم وصفها غالبًا على أنّها "صحيح"، تتحقّق من صحة الشرط، وتتطابق مع طريقة كتابة if للتحقق مما إذا كان ما سبق ناجحًا أو صحيحًا. يتم تقديم هذا العنوان على أنّه assert(...) أو assert.ok(...)، ويشمل قيمة واحدة بالإضافة إلى تعليق اختياري.

  • اختبار المساواة، كما هو الحال في مثال اختبار الرياضيات، والذي تتوقع فيه أن تكون قيمة الإرجاع أو الحالة لكائن ما مساوية لقيمة جيدة معروفة. تُستخدم هذه لإنشاءات المساواة الأولية (مثل الأرقام والسلاسل) أو المساواة المرجعية (هل هذه هي الكائن نفسه). تتوفّر أدناه معلومات حصرية من خلال مقارنة بين == و===.

    • يميّز JavaScript بين المساواة الحرة (==) والمساواة الصارمة (===). توفّر لك معظم مكتبات الاختبار الطريقتَين assert.equal وassert.strictEqual على التوالي.
  • عمليات تحقق المساواة العميقة، والتي تعمل على توسيع عمليات التحقق من المساواة لتشمل التحقق من محتويات الكائنات والصفائف، وغيرها من أنواع البيانات الأكثر تعقيدًا، بالإضافة إلى المنطق الداخلي لاجتياز العناصر لمقارنتها. هذه الأمور مهمة لأن لغة JavaScript لا تحتوي على طريقة مضمّنة لمقارنة محتويات كائنين أو مصفوفتين. على سبيل المثال، تكون القيمة [1,2,3] == [1,2,3] دائمًا false. غالبًا ما تتضمّن أطر عمل الاختبار أدوات المساعدة deepEqual أو deepStrictEqual.

عادةً ما تأخذ مساعدات التأكيد التي تقارن بين قيمتين (بدلاً من اختبار "truthy" فقط) وسيطتين أو ثلاث وسيطات:

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

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

  test('JWT parse', () => {
    const json = decodeJwt('eyJieSI6InNhbXRob3Ii…');

    assert.ok(json.payload.admin, 'user should be admin');
    assert.deepEqual(json.payload.groups, ['role:Admin', 'role:Submitter']);
    assert.equal(json.header.alg, 'RS265')
    assert.isAbove(json.payload.exp, +new Date(), 'expiry must be in future')
  });

يستخدم Vitest مكتبة تأكيد شاي داخليًا لتوفير مساعدات التأكيد، وقد يكون من المفيد مراجعة مراجعها لمعرفة التأكيدات والمساعدات التي قد تتناسب مع التعليمة البرمجية.

إقرارات طليقة ومصطلح BDD

يفضّل بعض المطوّرين أسلوب التأكيد الذي يمكن تسميته بالتطوير المبني على السلوك (BDD) أو التأكيدات بأسلوب فصيح. تُسمى هذه أيضًا أدوات المساعدة "المتوقعة"، لأن نقطة الدخول للتحقق من التوقعات هي طريقة تُسمّى expect().

توقَّع أن يتصرف المساعدون بالطريقة نفسها التي تتّبعها التأكيدات المكتوبة كاستدعاءات بسيطة، مثل assert.ok أو assert.strictDeepEquals، إلا أنّ بعض المطوّرين يجدون أنّها أسهل في القراءة. قد يظهر تأكيد BDD على النحو التالي:

// A failure here would generate "Expect result to be an array that does include 42"
const result = await possibleMeaningsOfLife();
expect(result).to.be.an('array').that.does.include(42);

// or a simpler form
expect(result).toBe('array').toContainEqual(42);

// the same in assert might be
assert.typeOf(result, 'array', 'Expected the result to be an array');
assert.include(result, 42, 'Expected the result to include 42');

يعمل هذا النمط من التأكيدات بسبب تقنية تُسمّى تسلسل الأساليب، حيث يمكن باستمرار تسلسل الكائن الذي يعرضه expect بتسلسل مع استدعاءات طرق أخرى. بعض أجزاء الطلب، بما في ذلك to.be وthat.does في المثال السابق، لا تحتوي على وظيفة ويتم تضمينها فقط لتسهيل قراءة الطلب وإنشاء تعليق تلقائي في حال تعذُّر الاختبار. (على وجه الخصوص، لا يدعم expect عادةً تعليقًا اختياريًا، لأنّ التسلسل يجب أن يصف الخطأ بوضوح).

يدعم العديد من أطر الاختبار كلاً من Fluent/BDD وتأكيدات البيانات المنتظمة. يصدِّر Vitest، على سبيل المثال، كلا النهجَين الذي يتّبعه "شاي" ولديه نهج أكثر إيجازًا تجاه BDD. من ناحية أخرى، لا يتضمن Jest إلا طريقة توقع تلقائيًا.

تجميع الاختبارات في الملفات

عند كتابة الاختبارات، نميل في الواقع إلى تقديم مجموعات ضمنية، بدلاً من أن تكون جميع الاختبارات في ملف واحد، من الشائع كتابة الاختبارات عبر ملفات متعددة. في الواقع، لا يعرف عدّاء الاختبار عادةً أن الملف مخصّص للاختبار إلا بسبب فلتر محدد مسبقًا أو تعبير عادي. على سبيل المثال، يتضمّن الملف جميع الملفات في مشروعك التي تنتهي بامتداد مثل "test.jsx" أو ".spec.ts" (".test" و "spec." بالإضافة إلى عدد من الإضافات الصالحة).

تميل اختبارات المكونات إلى وجودها في ملف نظير للمكون الذي يخضع للاختبار، كما هو الحال في هيكل الدليل التالي:

قائمة بالملفات في دليل، بما في ذلك UserList.tsx وUserList.test.tsx.
ملف مكوّن وملف اختبار ذي صلة

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

إجراء اختبارات جماعية داخل الملفات

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

كما هو الحال مع التأكيدات، هناك تكافؤ معياري إلى حد ما في Fluent/BDD مع اختبارات التجميع. تتم مقارنة بعض الأمثلة النموذجية في الرمز التالي:

// traditional/TDD
suite('math tests', () => {
  test('handle zero values', () => {
    assert.equal(fibonacci(0), 0);
  });
});

// Fluent/BDD
describe('math tests', () => {
  it('should handle zero values', () => {
    expect(fibonacci(0)).toBe(0);
  });
})

في معظم أُطر العمل، يتم تطبيق إجراءات مماثلة من suite وdescribe، مثل test وit، بدلاً من الاختلافات الكبيرة بين استخدام expect وassert لكتابة التأكيدات.

تتمتع الأدوات الأخرى بأساليب مختلفة تمامًا لترتيب المجموعات والاختبارات. على سبيل المثال، يتيح مشغّل الاختبار المضمَّن في Node.js استخدام استدعاءات التداخل مع test() لإنشاء عرض هرمي للاختبار ضمنيًا. مع ذلك، يسمح Vitest فقط بهذا النوع من التداخل باستخدام suite() ولن يشغّل test() معرّفًا داخل test() أخرى.

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

طرق مراحل النشاط

إنّ أحد أسباب تجميع اختباراتك، حتى ضمنيًا على المستوى الأعلى داخل ملف، هو توفير طرق الإعداد والإنهاء التي يتم إجراؤها لكل اختبار أو مرة واحدة لمجموعة من الاختبارات. توفر معظم أطر العمل أربع طرق:

لكل `test()` أو `it()` مرة واحدة للجناح
قبل إجراء الاختبارات `beforeEvery()` `beforeAll()`
بعد إجراء الاختبارات `afterEvery()` `afterAll()`

على سبيل المثال، قد ترغب في تعبئة قاعدة بيانات مستخدم افتراضية مسبقًا قبل كل اختبار، ومحوها بعد ذلك:

suite('user test', () => {
  beforeEach(() => {
    insertFakeUser('bob@example.com', 'hunter2');
  });
  afterEach(() => {
    clearAllUsers();
  });

  test('bob can login', async () => { … });
  test('alice can message bob', async () => { … });
});

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

نصائح عامة

فيما يلي بعض النصائح التي يجب تذكرها عند التفكير في هذه المبادئ الأساسية.

الأساسيات هي دليل

تذكّر أنّ الأدوات والإصدارات الأولية الواردة هنا، وفي الصفحات القليلة القادمة، لن تتطابق تمامًا مع Vitest أو Jest أو Mocha أو Web Test Runner أو أي إطار عمل آخر محدّد. بينما استخدمنا Vitest كدليل عام، تأكد من ربطها بإطار العمل الذي اخترته.

مزج التأكيدات ومطابقتها حسب الحاجة

الاختبارات هي في الأساس رموز برمجية يمكن أن تؤدي إلى حدوث أخطاء. سيحصل كل عدّاء على سمة test() أولية على الأرجح لوصف حالات الاختبار المختلفة.

ولكن إذا كان هذا العدّاء يوفّر أيضًا assert() وexpect() ومساعدات تأكيد، تذكّر أنّ هذا الجزء يتعلّق بالسهولة أكثر ويمكنك تخطّيه عند الحاجة. يمكنك تشغيل أي رمز برمجي قد يؤدي إلى ظهور خطأ، بما في ذلك مكتبات تأكيد التأكيد الأخرى أو عبارة if القديمة.

قد يساعدك إعداد IDE في إنقاذ الحياة

إن ضمان أن IDE، مثل VSCode، لديه إمكانية الوصول إلى الإكمال التلقائي والوثائق في أدوات الاختبار التي اخترتها يمكن أن يجعلك أكثر إنتاجية. على سبيل المثال، هناك أكثر من 100 طريقة على assert في مكتبة تأكيد تشاي، ويمكن أن يكون توفير المستندات الخاصة بالطريقة الصحيحة مضمّنًا.

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

// some.test.js
test('using test as a global', () => { … });

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

// some.test.js
import { test } from 'vitest';
test('using test as an import', () => { … });