خصائص اختبارات الوحدات الجيدة

الكاتب: قالب اقرأتاريخ النشر: آخر تحديث: وقت القراءة:
للقراءة
عدد الكلمات:
كلمة
عدد التعليقات: 0 تعليق
نبذة عن المقال: تعلم الخصائص الأساسية لكتابة اختبارات الوحدات (Unit Tests) الجيدة والفعالة. اكتشف كيف تجعل اختباراتك سريعة ومستقلة وقابلة للتكرار للحصول على كود موثوق

مرحباً بك في درس جديد! 

حتى الآن، تعلمنا ما هي اختبارات الوحدات (Unit Tests) وكتبنا أول اختباراتنا باستخدام Vitest. قد تظن أن المهمة تقتصر على كتابة أي اختبار يعمل. لكن في الواقع، هناك فرق شاسع بين كتابة اختبار يمر (pass) وكتابة اختبار جيد وفعال.

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

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

خصائص اختبارات الوحدات الجيدة

١. السرعة (Fast)

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

إذا كانت مجموعة الاختبارات (test suite) تستغرق دقائق لتكتمل، فستتوقف عن تشغيلها بانتظام. هذا يكسر حلقة التغذية الراجعة السريعة (fast feedback loop) التي تعد من أهم فوائد الاختبار. ستفقد الثقة في إجراء التغييرات لأنك لن تتمكن من التحقق من تأثيرها بسرعة.

تشبيه من الواقع

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

ما الذي يجعل الاختبارات بطيئة؟

عادةً، البطء يأتي من الاعتماد على عوامل خارجية:

  • عمليات الشبكة (Network requests): التواصل مع واجهات برمجية (APIs) خارجية.
  • قواعد البيانات (Database access): قراءة أو كتابة بيانات في قاعدة بيانات حقيقية.
  • نظام الملفات (File system): التعامل مع الملفات على القرص الصلب.

كيف نحافظ على سرعة الاختبارات؟

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

٢. الاستقلالية والانعزال (Independent and Isolated)

يجب أن يكون كل اختبار وحدة مستقلاً بذاته تماماً. هذا يعني أن نتيجة اختبار معين يجب ألا تعتمد أبداً على نتيجة اختبار آخر، كما يجب ألا يؤثر تشغيل اختبار ما على أي اختبار يليه.

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

مثال على اختبارات غير مستقلة:

لنفترض أن لدينا مصفوفة عامة (global array) ونختبر وظائف تتعامل معها.

JavaScript
// shoppingCart.js
const cart = [];

export function addItem(item) {
  cart.push(item);
}

export function getCartSize() {
  return cart.length;
}

export function clearCart() {
  // Let's pretend we forgot to implement this or it's not used in tests
  cart.length = 0;
}

الآن، لنلقِ نظرة على اختبارات سيئة التصميم:

JavaScript
// shoppingCart.test.js
import { describe, it, expect } from 'vitest';
import { addItem, getCartSize } from './shoppingCart';

describe('shoppingCart', () => {
  it('should add an item to the cart', () => {
    addItem('laptop');
    expect(getCartSize()).toBe(1);
  });

  // This test is problematic!
  it('should start with an empty cart', () => {
    // This test will fail if the test above runs first,
    // because the cart already contains 'laptop'.
    expect(getCartSize()).toBe(0);
  });
});

في هذا المثال، سيفشل الاختبار الثاني لأنه يعتمد على حالة (state) تركتها الاختبار الأول. هذا انتهاك لمبدأ الاستقلالية.

الحل: إعادة الضبط قبل كل اختبار

لضمان استقلالية الاختبارات، يجب علينا إعادة ضبط الحالة (state) قبل أو بعد كل اختبار. معظم أطر عمل الاختبار توفر دوال مساعدة مثل beforeEach و afterEach لهذا الغرض.

JavaScript
// shoppingCart.test.js (The Correct Way)
import { describe, it, expect, beforeEach } from 'vitest';
import { addItem, getCartSize, clearCart } from './shoppingCart';

describe('shoppingCart', () => {
  // This function runs before each 'it' block in this 'describe' block.
  beforeEach(() => {
    clearCart(); // Reset the state to a known baseline
  });

  it('should add an item to the cart', () => {
    addItem('laptop');
    expect(getCartSize()).toBe(1);
  });

  it('should start with an empty cart', () => {
    // Now this test will always pass because the cart is cleared before it runs.
    expect(getCartSize()).toBe(0);
  });
});

نصيحة احترافية

دائماً ابدأ اختباراتك من حالة أولية نظيفة ومعروفة. لا تفترض أبداً أن الاختبار السابق ترك البيئة في حالة معينة.

٣. قابلية التكرار (Repeatable)

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

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

ما الذي يسبب الاختبارات غير القابلة للتكرار؟

  • الاعتماد على الوقت الحالي: استخدام new Date() أو Date.now() يمكن أن يسبب مشاكل.
  • الأرقام العشوائية: الاعتماد على Math.random() يجعل النتائج غير متوقعة.
  • البيئة الخارجية: الاعتماد على خدمة خارجية قد تكون غير متاحة مؤقتاً.
  • الاعتماد على ترتيب التنفيذ: كما رأينا في النقطة السابقة.

مثال على اختبار غير قابل للتكرار:

لنفترض أن لدينا وظيفة تولد رسالة ترحيب خاصة بناءً على وقت معين من اليوم.

JavaScript
// greeting.js
export function getGreeting() {
  const hour = new Date().getHours();
  if (hour < 12) {
    return 'Good morning!';
  }
  return 'Good afternoon!';
}
JavaScript
// greeting.test.js
import { describe, it, expect } from 'vitest';
import { getGreeting } from './greeting';

describe('getGreeting', () => {
  it('should return "Good morning!" in the morning', () => {
    // This test will only pass if run before 12 PM.
    // It will fail in the afternoon.
    expect(getGreeting()).toBe('Good morning!');
  });
});

هذا الاختبار غير موثوق به على الإطلاق. الحل لهذه المشكلة هو التحكم في "الوقت" داخل الاختبار، وهو ما سنتعلمه لاحقاً عند الحديث عن الـ Mocks.

٤. التحقق الذاتي (Self-Validating)

يجب أن تكون نتيجة الاختبار واضحة وصريحة: إما نجاح (pass) أو فشل (fail). لا ينبغي أن يتطلب الاختبار أي تدخل يدوي أو تفسير للنتائج. يجب ألا تضطر إلى النظر في console.log لتحديد ما إذا كان الكود يعمل بشكل صحيح أم لا.

الاختبار هو من يقوم بعملية التحقق بنفسه عبر التأكيدات (Assertions).

مثال سيء (يتطلب تفسيراً يدوياً):

JavaScript
import { add } from './calculator';

it('should add two numbers', () => {
  const result = add(2, 3);
  console.log('Result is:', result); // Now I have to manually check if this is 5
});

مثال جيد (يتحقق ذاتياً):

JavaScript
import { add } from './calculator';

it('should add two numbers', () => {
  const result = add(2, 3);
  expect(result).toBe(5); // The test framework validates the result automatically.
});

إذا كانت النتيجة غير 5، سيخبرك إطار العمل Vitest بذلك بوضوح، مع تحديد القيمة المتوقعة والقيمة الفعلية. هذا هو جوهر التحقق الذاتي.

٥. الدقة والتركيز (Targeted and Timely)

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

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

فوائد الاختبارات المركزة:

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

مثال سيء (اختبار واحد يفعل كل شيء):

JavaScript
// Bad: One test for multiple behaviors
it('should handle user authentication', () => {
  // 1. Test registration
  const user = register('test@example.com', 'password');
  expect(user.email).toBe('test@example.com');

  // 2. Test login
  const token = login('test@example.com', 'password');
  expect(token).toBeDefined();

  // 3. Test password change
  const success = changePassword(token, 'newPassword');
  expect(success).toBe(true);
});

إذا فشل هذا الاختبار، هل المشكلة في التسجيل أم تسجيل الدخول أم تغيير كلمة المرور؟

مثال جيد (اختبارات منفصلة ومركزة):

JavaScript
// Good: Separate tests for each behavior
describe('user authentication', () => {
  it('should register a new user successfully', () => {
    const user = register('test@example.com', 'password');
    expect(user.email).toBe('test@example.com');
  });

  it('should allow a registered user to log in', () => {
    const token = login('test@example.com', 'password');
    expect(token).toBeDefined();
  });

  it('should allow a logged-in user to change their password', () => {
    const token = login('test@example.com', 'password'); // Assume login works
    const success = changePassword(token, 'newPassword');
    expect(success).toBe(true);
  });
});

الآن، إذا فشل اختبار should allow a registered user to log in، فأنت تعرف بالضبط أين تبحث عن الخطأ.

ملاحظة هامة

كلمة "Timely" (في الوقت المناسب) تشير أيضاً إلى أن الاختبارات يجب أن تُكتب في أقرب وقت ممكن من كتابة الكود الفعلي، أو حتى قبله كما في منهجية التطوير الموجه بالاختبار (Test-Driven Development - TDD) التي ناقشناها سابقاً.

الخلاصة

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

  • سريعة (Fast): تعمل في أجزاء من الثانية لتوفير تغذية راجعة فورية.
  • مستقلة (Independent): لا تتأثر بالاختبارات الأخرى أو تؤثر فيها.
  • قابلة للتكرار (Repeatable): تعطي نفس النتيجة في كل مرة وفي أي بيئة.
  • ذاتية التحقق (Self-Validating): نتيجتها واضحة (نجاح/فشل) بدون تدخل يدوي.
  • مركزة (Targeted): تختبر مفهوماً منطقياً واحداً فقط في كل مرة.

بينما تواصل رحلتك في هذه الدورة، حاول أن تبقي هذه المبادئ في ذهنك عند كتابة كل اختبار. الآن بعد أن فهمنا كيف يبدو الاختبار الجيد، حان الوقت للتعمق في الأدوات التي نستخدمها لكتابة تأكيدات قوية وواضحة في الدرس التالي: استخدام الـ Matchers.

قد تُعجبك هذه المشاركات

إرسال تعليق

ليست هناك تعليقات

7627059358572141466

العلامات المرجعية

قائمة العلامات المرجعية فارغة ... قم بإضافة مقالاتك الآن

    البحث