العربية

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

التطوير المُوجَّه بالاختبار في جافاسكريبت: دليل شامل للمطورين العالميين

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

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

ما هو التطوير المُوجَّه بالاختبار (TDD)؟

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

إيقاع TDD: الأحمر-الأخضر-إعادة الهيكلة

هذه الدورة المكونة من ثلاث خطوات هي نبض TDD. فهم وممارسة هذا الإيقاع أمر أساسي لإتقان هذه التقنية.

بمجرد اكتمال الدورة لجزء صغير من الوظائف، تبدأ مرة أخرى باختبار فاشل جديد للجزء التالي.

القوانين الثلاثة لـ TDD

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

  1. لا يجوز لك كتابة أي كود إنتاجي إلا لغرض جعل اختبار وحدة فاشل ينجح.
  2. لا يجوز لك كتابة أكثر من اللازم من اختبار الوحدة لجعله يفشل؛ وأخطاء الترجمة (compilation failures) تعتبر فشلاً.
  3. لا يجوز لك كتابة أكثر من اللازم من كود الإنتاج لجعله يجتاز اختبار الوحدة الفاشل الوحيد.

اتباع هذه القوانين يجبرك على الدخول في دورة الأحمر-الأخضر-إعادة الهيكلة ويضمن أن 100٪ من كود الإنتاج الخاص بك مكتوب لتلبية متطلب محدد ومُختبَر.

لماذا يجب عليك اعتماد TDD؟ الحالة التجارية العالمية

بينما يقدم TDD فوائد هائلة للمطورين الأفراد، فإن قوته الحقيقية تتحقق على مستوى الفريق والعمل، خاصة في البيئات الموزعة عالميًا.

إعداد بيئة TDD الخاصة بك في جافاسكريبت

للبدء مع TDD في جافاسكريبت، تحتاج إلى بعض الأدوات. يقدم النظام البيئي الحديث لجافاسكريبت خيارات ممتازة.

المكونات الأساسية لمكدس الاختبار

لبساطته وطبيعته المتكاملة، سوف نستخدم Jest في أمثلتنا. إنه خيار ممتاز للفرق التي تبحث عن تجربة "بدون تكوين".

إعداد خطوة بخطوة مع Jest

لنقم بإعداد مشروع جديد لـ TDD.

1. تهيئة مشروعك: افتح الطرفية (terminal) وأنشئ مجلد مشروع جديد.

mkdir js-tdd-project
cd js-tdd-project
npm init -y

2. تثبيت Jest: أضف Jest إلى مشروعك كتبعية تطوير.

npm install --save-dev jest

3. تكوين سكربت الاختبار: افتح ملف `package.json`. ابحث عن قسم `"scripts"` وقم بتعديل سكربت `"test"`. يوصى بشدة أيضًا بإضافة سكربت `"test:watch"`، وهو لا يقدر بثمن لسير عمل TDD.

"scripts": {
  "test": "jest",
  "test:watch": "jest --watchAll"
}

علامة `--watchAll` تخبر Jest بإعادة تشغيل الاختبارات تلقائيًا كلما تم حفظ ملف. هذا يوفر ملاحظات فورية، وهو مثالي لدورة الأحمر-الأخضر-إعادة الهيكلة.

هذا كل شيء! بيئتك جاهزة. سيجد Jest تلقائيًا ملفات الاختبار التي تسمى `*.test.js` أو `*.spec.js` أو الموجودة في مجلد `__tests__`.

TDD في الممارسة: بناء وحدة `CurrencyConverter`

لنطبق دورة TDD على مشكلة عملية ومفهومة عالميًا: تحويل الأموال بين العملات. سنبني وحدة `CurrencyConverter` خطوة بخطوة.

التكرار 1: تحويل بسيط بسعر ثابت

🔴 الأحمر: اكتب أول اختبار فاشل

أول متطلب لدينا هو تحويل مبلغ معين من عملة إلى أخرى باستخدام سعر ثابت. أنشئ ملفًا جديدًا باسم `CurrencyConverter.test.js`.

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

describe('CurrencyConverter', () => {
  it('should convert an amount from USD to EUR correctly', () => {
    // Arrange
    const amount = 10; // 10 USD
    const expected = 9.2; // Assuming a fixed rate of 1 USD = 0.92 EUR

    // Act
    const result = CurrencyConverter.convert(amount, 'USD', 'EUR');

    // Assert
    expect(result).toBe(expected);
  });
});

الآن، قم بتشغيل مراقب الاختبار من الطرفية الخاصة بك:

npm run test:watch

سيفشل الاختبار فشلاً ذريعًا. سيبلغ Jest شيئًا مثل `TypeError: Cannot read properties of undefined (reading 'convert')`. هذه هي حالة الأحمر لدينا. يفشل الاختبار لأن `CurrencyConverter` غير موجود.

🟢 الأخضر: اكتب أبسط كود للنجاح

الآن، لنجعل الاختبار ينجح. أنشئ `CurrencyConverter.js`.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

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

🔵 إعادة الهيكلة: تحسين الكود

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

التكرار 2: التعامل مع العملات غير المعروفة

🔴 الأحمر: اكتب اختبارًا لعملة غير صالحة

ماذا يجب أن يحدث إذا حاولنا التحويل إلى عملة لا نعرفها؟ ربما يجب أن يطلق خطأ. دعنا نحدد هذا السلوك في اختبار جديد في `CurrencyConverter.test.js`.

// In CurrencyConverter.test.js, inside the describe block

it('should throw an error for unknown currencies', () => {
  // Arrange
  const amount = 10;

  // Act & Assert
  // We wrap the function call in an arrow function for Jest's toThrow to work.
  expect(() => {
    CurrencyConverter.convert(amount, 'USD', 'XYZ');
  }).toThrow('Unknown currency: XYZ');
});

احفظ الملف. يظهر مشغل الاختبار على الفور فشلاً جديدًا. إنه أحمر لأن الكود الخاص بنا لا يطلق خطأ؛ إنه يحاول الوصول إلى `rates['USD']['XYZ']`، مما ينتج عنه `TypeError`. لقد حدد اختبارنا الجديد هذا الخلل بشكل صحيح.

🟢 الأخضر: اجعل الاختبار الجديد ينجح

دعنا نعدل `CurrencyConverter.js` لإضافة التحقق.

// CurrencyConverter.js
const rates = {
  USD: {
    EUR: 0.92,
    GBP: 0.80
  },
  EUR: {
    USD: 1.08
  }
};

const CurrencyConverter = {
  convert(amount, from, to) {
    if (!rates[from] || !rates[from][to]) {
      // Determine which currency is unknown for a better error message
      const unknownCurrency = !rates[from] ? from : to;
      throw new Error(`Unknown currency: ${unknownCurrency}`);
    }
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

احفظ الملف. كلا الاختبارين ينجحان الآن. لقد عدنا إلى الأخضر.

🔵 إعادة الهيكلة: قم بتنظيفه

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

التكرار 3: جلب الأسعار بشكل غير متزامن

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

🔴 الأحمر: اكتب اختبارًا غير متزامن يحاكي استدعاء API

أولاً، نحتاج إلى إعادة هيكلة محولنا. سيحتاج الآن إلى أن يكون فئة (class) يمكننا إنشاء مثيل لها، ربما مع عميل API. سنحتاج أيضًا إلى محاكاة واجهة برمجة تطبيقات `fetch`. يجعل Jest هذا سهلاً.

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

// CurrencyConverter.test.js
const CurrencyConverter = require('./CurrencyConverter');

// Mock the external dependency
global.fetch = jest.fn();

beforeEach(() => {
  // Clear mock history before each test
  fetch.mockClear();
});

describe('CurrencyConverter', () => {
  it('should fetch rates and convert correctly', async () => {
    // Arrange
    // Mock the successful API response
    fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ rates: { EUR: 0.92 } })
    });

    const converter = new CurrencyConverter('https://api.exchangerates.com');
    const amount = 10; // 10 USD

    // Act
    const result = await converter.convert(amount, 'USD', 'EUR');

    // Assert
    expect(result).toBe(9.2);
    expect(fetch).toHaveBeenCalledTimes(1);
    expect(fetch).toHaveBeenCalledWith('https://api.exchangerates.com/latest?base=USD');
  });

  // We'd also add tests for API failures, etc.
});

تشغيل هذا سيؤدي إلى بحر من الأحمر. `CurrencyConverter` القديم لدينا ليس فئة، وليس لديه دالة `async`، ولا يستخدم `fetch`.

🟢 الأخضر: تنفيذ المنطق غير المتزامن

الآن، دعنا نعيد كتابة `CurrencyConverter.js` لتلبية متطلبات الاختبار.

// CurrencyConverter.js
class CurrencyConverter {
  constructor(apiUrl) {
    this.apiUrl = apiUrl;
  }

  async convert(amount, from, to) {
    const response = await fetch(`${this.apiUrl}/latest?base=${from}`);
    if (!response.ok) {
      throw new Error('Failed to fetch exchange rates.');
    }

    const data = await response.json();
    const rate = data.rates[to];

    if (!rate) {
      throw new Error(`Unknown currency: ${to}`);
    }

    // Simple rounding to avoid floating point issues in tests
    const convertedAmount = amount * rate;
    return parseFloat(convertedAmount.toFixed(2));
  }
}

module.exports = CurrencyConverter;

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

🔵 إعادة الهيكلة: تحسين الكود غير المتزامن

دالة `convert` تفعل الكثير: الجلب، معالجة الأخطاء، التحليل، والحساب. يمكننا إعادة هيكلة هذا عن طريق إنشاء فئة `RateFetcher` منفصلة مسؤولة فقط عن الاتصال بـ API. سيستخدم `CurrencyConverter` الخاص بنا بعد ذلك هذا الجالب. هذا يتبع مبدأ المسؤولية الواحدة ويجعل كلتا الفئتين أسهل في الاختبار والصيانة. يرشدنا TDD نحو هذا التصميم الأنظف.

أنماط TDD الشائعة والأنماط المضادة

أثناء ممارسة TDD، ستكتشف أنماطًا تعمل بشكل جيد وأنماطًا مضادة تسبب احتكاكًا.

أنماط جيدة للمتابعة

أنماط مضادة يجب تجنبها

TDD في دورة حياة التطوير الأوسع

لا يوجد TDD في فراغ. يتكامل بشكل جميل مع ممارسات أجايل و DevOps الحديثة، خاصة للفرق العالمية.

الخلاصة: رحلتك مع TDD

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

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

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