فارسی

بر توسعه آزمون‌محور (TDD) در جاوا اسکریپت مسلط شوید. این راهنمای جامع چرخه قرمز-سبز-بازسازی، پیاده‌سازی عملی با Jest و بهترین شیوه‌ها برای توسعه مدرن را پوشش می‌دهد.

توسعه آزمون‌محور در جاوا اسکریپت: راهنمای جامع برای توسعه‌دهندگان جهانی

این سناریو را تصور کنید: به شما وظیفه داده شده تا بخش حساسی از کد را در یک سیستم بزرگ و قدیمی تغییر دهید. احساس ترس می‌کنید. آیا تغییر شما بخش دیگری را خراب خواهد کرد؟ چگونه می‌توانید مطمئن باشید که سیستم هنوز طبق انتظار کار می‌کند؟ این ترس از تغییر، یک بیماری شایع در توسعه نرم‌افزار است که اغلب به پیشرفت کند و برنامه‌های شکننده منجر می‌شود. اما اگر راهی برای ساخت نرم‌افزار با اطمینان وجود داشت، راهی که یک شبکه ایمنی ایجاد کند تا خطاها را قبل از رسیدن به محیط پروداکشن شناسایی کند؟ این همان وعده توسعه آزمون‌محور (TDD) است.

TDD صرفاً یک تکنیک تست‌نویسی نیست؛ بلکه یک رویکرد منضبط برای طراحی و توسعه نرم‌افزار است. این رویکرد، مدل سنتی «کد بنویس، سپس تست کن» را معکوس می‌کند. با TDD، شما یک تست می‌نویسید که قبل از نوشتن کد اصلی برای پاس کردن آن، شکست می‌خورد. این وارونگی ساده، پیامدهای عمیقی برای کیفیت کد، طراحی و قابلیت نگهداری دارد. این راهنما نگاهی جامع و عملی به پیاده‌سازی TDD در جاوا اسکریپت خواهد داشت که برای مخاطبان جهانی از توسعه‌دهندگان حرفه‌ای طراحی شده است.

توسعه آزمون‌محور (TDD) چیست؟

در هسته خود، توسعه آزمون‌محور یک فرآیند توسعه است که بر تکرار یک چرخه توسعه بسیار کوتاه تکیه دارد. به جای نوشتن ویژگی‌ها و سپس تست کردن آن‌ها، TDD اصرار دارد که تست ابتدا نوشته شود. این تست به ناچار شکست خواهد خورد زیرا ویژگی هنوز وجود ندارد. وظیفه توسعه‌دهنده این است که ساده‌ترین کد ممکن را برای پاس کردن آن تست خاص بنویسد. پس از پاس شدن تست، کد تمیز و بهبود داده می‌شود. این حلقه بنیادین به عنوان چرخه «قرمز-سبز-بازسازی» شناخته می‌شود.

ریتم TDD: قرمز-سبز-بازسازی

این چرخه سه‌مرحله‌ای، تپش قلب TDD است. درک و تمرین این ریتم برای تسلط بر این تکنیک اساسی است.

هنگامی که چرخه برای یک بخش کوچک از عملکرد کامل شد، شما دوباره با یک تست شکست‌خورده جدید برای بخش بعدی شروع می‌کنید.

سه قانون TDD

رابرت سی. مارتین (که اغلب با نام «عمو باب» شناخته می‌شود)، یکی از چهره‌های کلیدی در جنبش نرم‌افزار چابک، سه قانون ساده را تعریف کرد که انضباط TDD را مدون می‌کنند:

  1. شما مجاز به نوشتن هیچ کد اصلی نیستید، مگر اینکه برای پاس کردن یک تست واحد شکست‌خورده باشد.
  2. شما مجاز به نوشتن بیش از آن مقدار از یک تست واحد که برای شکست خوردن کافی است، نیستید؛ و خطاهای کامپایل نیز شکست محسوب می‌شوند.
  3. شما مجاز به نوشتن بیش از آن مقدار از کد اصلی که برای پاس کردن همان یک تست واحد شکست‌خورده کافی است، نیستید.

پیروی از این قوانین شما را به چرخه قرمز-سبز-بازسازی وادار می‌کند و تضمین می‌کند که ۱۰۰٪ کد اصلی شما برای برآورده کردن یک نیازمندی خاص و تست‌شده نوشته شده است.

چرا باید TDD را اتخاذ کنید؟ توجیه تجاری جهانی

در حالی که TDD مزایای بی‌شماری برای توسعه‌دهندگان فردی دارد، قدرت واقعی آن در سطح تیم و کسب‌وکار، به ویژه در محیط‌های توزیع‌شده جهانی، محقق می‌شود.

راه‌اندازی محیط TDD جاوا اسکریپت شما

برای شروع کار با TDD در جاوا اسکریپت، به چند ابزار نیاز دارید. اکوسیستم مدرن جاوا اسکریپت انتخاب‌های عالی ارائه می‌دهد.

اجزای اصلی یک پشته تست

به دلیل سادگی و ماهیت یکپارچه آن، ما از Jest برای مثال‌های خود استفاده خواهیم کرد. این یک انتخاب عالی برای تیم‌هایی است که به دنبال یک تجربه «بدون پیکربندی» هستند.

راه‌اندازی گام به گام با Jest

بیایید یک پروژه جدید برای TDD راه‌اندازی کنیم.

۱. پروژه خود را مقداردهی اولیه کنید: ترمینال خود را باز کرده و یک دایرکتوری پروژه جدید ایجاد کنید.

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

۲. نصب Jest: Jest را به عنوان یک وابستگی توسعه (development dependency) به پروژه خود اضافه کنید.

npm install --save-dev jest

۳. پیکربندی اسکریپت تست: فایل `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` را گام به گام خواهیم ساخت.

تکرار ۱: تبدیل ساده با نرخ ثابت

🔴 قرمز: اولین تست شکست‌خورده را بنویسید

اولین نیازمندی ما تبدیل یک مقدار مشخص از یک ارز به ارز دیگر با استفاده از یک نرخ ثابت است. یک فایل جدید به نام `CurrencyConverter.test.js` ایجاد کنید.

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

describe('CurrencyConverter', () => {
  it('should convert an amount from USD to EUR correctly', () => {
    // آماده‌سازی
    const amount = 10; // 10 دلار آمریکا
    const expected = 9.2; // با فرض نرخ ثابت 1 دلار آمریکا = 0.92 یورو

    // اجرا
    const result = CurrencyConverter.convert(amount, 'USD', 'EUR');

    // تایید
    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` کمی خشک است. فعلاً به اندازه کافی تمیز است. مهمترین چیز این است که ما یک ویژگی کارا داریم که توسط یک تست محافظت می‌شود. بیایید به نیازمندی بعدی برویم.

تکرار ۲: مدیریت ارزهای ناشناخته

🔴 قرمز: یک تست برای ارز نامعتبر بنویسید

چه اتفاقی باید بیفتد اگر سعی کنیم به ارزی تبدیل کنیم که نمی‌شناسیم؟ احتمالاً باید یک خطا پرتاب کند. بیایید این رفتار را در یک تست جدید در `CurrencyConverter.test.js` تعریف کنیم.

// در CurrencyConverter.test.js، داخل بلوک describe

it('should throw an error for unknown currencies', () => {
  // آماده‌سازی
  const amount = 10;

  // اجرا و تایید
  // ما فراخوانی تابع را در یک تابع پیکانی (arrow function) قرار می‌دهیم تا toThrow در Jest کار کند.
  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]) {
      // برای پیام خطای بهتر، مشخص کنید کدام ارز ناشناخته است
      const unknownCurrency = !rates[from] ? from : to;
      throw new Error(`Unknown currency: ${unknownCurrency}`);
    }
    return amount * rates[from][to];
  }
};

module.exports = CurrencyConverter;

فایل را ذخیره کنید. هر دو تست اکنون پاس می‌شوند. ما به وضعیت سبز برگشتیم.

🔵 بازسازی: آن را تمیز کنید

تابع `convert` ما در حال رشد است. منطق اعتبارسنجی با محاسبه مخلوط شده است. ما می‌توانیم اعتبارسنجی را به یک تابع خصوصی جداگانه استخراج کنیم تا خوانایی را بهبود ببخشیم، اما در حال حاضر، هنوز قابل مدیریت است. نکته کلیدی این است که ما آزادی انجام این تغییرات را داریم زیرا تست‌های ما به ما خواهند گفت که آیا چیزی را خراب کرده‌ایم یا نه.

تکرار ۳: واکشی ناهمزمان نرخ‌ها

کدنویسی سخت نرخ‌ها واقع‌بینانه نیست. بیایید ماژول خود را بازسازی کنیم تا نرخ‌ها را از یک API خارجی (شبیه‌سازی شده) واکشی کند.

🔴 قرمز: یک تست ناهمزمان بنویسید که یک فراخوانی API را شبیه‌سازی می‌کند

ابتدا، باید مبدل خود را بازسازی کنیم. اکنون باید یک کلاس باشد که بتوانیم آن را نمونه‌سازی کنیم، شاید با یک کلاینت API. همچنین باید API `fetch` را شبیه‌سازی کنیم. Jest این کار را آسان می‌کند.

بیایید فایل تست خود را برای سازگاری با این واقعیت جدید و ناهمزمان بازنویسی کنیم. ما با تست کردن دوباره مسیر موفقیت‌آمیز شروع می‌کنیم.

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

// وابستگی خارجی را شبیه‌سازی (mock) می‌کنیم
global.fetch = jest.fn();

beforeEach(() => {
  // تاریخچه mock را قبل از هر تست پاک می‌کنیم
  fetch.mockClear();
});

describe('CurrencyConverter', () => {
  it('should fetch rates and convert correctly', async () => {
    // آماده‌سازی
    // پاسخ موفق API را شبیه‌سازی می‌کنیم
    fetch.mockResolvedValueOnce({
      json: () => Promise.resolve({ rates: { EUR: 0.92 } })
    });

    const converter = new CurrencyConverter('https://api.exchangerates.com');
    const amount = 10; // 10 دلار آمریکا

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

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

  // ما همچنین تست‌هایی برای شکست‌های API و غیره اضافه خواهیم کرد.
});

اجرای این کد منجر به دریایی از قرمز خواهد شد. `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}`);
    }

    // گرد کردن ساده برای جلوگیری از مشکلات ممیز شناور در تست‌ها
    const convertedAmount = amount * rate;
    return parseFloat(convertedAmount.toFixed(2));
  }
}

module.exports = CurrencyConverter;

وقتی ذخیره می‌کنید، تست باید سبز شود. توجه داشته باشید که ما همچنین منطق گرد کردن را برای مدیریت عدم دقت‌های ممیز شناور اضافه کردیم، یک مسئله رایج در محاسبات مالی.

🔵 بازسازی: کد ناهمزمان را بهبود دهید

متد `convert` کارهای زیادی انجام می‌دهد: واکشی، مدیریت خطا، تجزیه و محاسبه. ما می‌توانیم این را با ایجاد یک کلاس `RateFetcher` جداگانه که فقط مسئول ارتباط API است، بازسازی کنیم. سپس `CurrencyConverter` ما از این واکشی‌کننده استفاده می‌کند. این از اصل مسئولیت واحد پیروی می‌کند و تست و نگهداری هر دو کلاس را آسان‌تر می‌کند. TDD ما را به سمت این طراحی تمیزتر راهنمایی می‌کند.

الگوهای رایج و ضدالگوهای TDD

همانطور که TDD را تمرین می‌کنید، الگوهایی را کشف خواهید کرد که به خوبی کار می‌کنند و ضدالگوهایی که باعث اصطکاک می‌شوند.

الگوهای خوب برای پیروی

ضدالگوها برای اجتناب

TDD در چرخه عمر توسعه گسترده‌تر

TDD در خلاء وجود ندارد. این به زیبایی با شیوه‌های مدرن چابک و DevOps، به ویژه برای تیم‌های جهانی، ادغام می‌شود.

نتیجه‌گیری: سفر شما با TDD

توسعه آزمون‌محور چیزی فراتر از یک استراتژی تست است—این یک تغییر پارادایم در نحوه رویکرد ما به توسعه نرم‌افزار است. این فرهنگ کیفیت، اطمینان و همکاری را پرورش می‌دهد. چرخه قرمز-سبز-بازسازی یک ریتم ثابت را فراهم می‌کند که شما را به سمت کد تمیز، قوی و قابل نگهداری هدایت می‌کند. مجموعه تست حاصل به یک شبکه ایمنی تبدیل می‌شود که تیم شما را از رگرسیون‌ها محافظت می‌کند و به مستندات زنده‌ای تبدیل می‌شود که اعضای جدید را به تیم وارد می‌کند.

منحنی یادگیری می‌تواند شیب‌دار به نظر برسد و سرعت اولیه ممکن است کندتر به نظر برسد. اما سود بلندمدت در کاهش زمان دیباگ، بهبود طراحی نرم‌افزار و افزایش اعتماد به نفس توسعه‌دهنده غیرقابل اندازه‌گیری است. سفر به تسلط بر TDD، سفری از انضباط و تمرین است.

از امروز شروع کنید. یک ویژگی کوچک و غیرحیاتی را در پروژه بعدی خود انتخاب کنید و به این فرآیند متعهد شوید. ابتدا تست را بنویسید. شکست آن را تماشا کنید. آن را پاس کنید. و سپس، مهم‌تر از همه، بازسازی کنید. اطمینانی را که از یک مجموعه تست سبز به دست می‌آید تجربه کنید، و به زودی تعجب خواهید کرد که چگونه تا به حال به روش دیگری نرم‌افزار ساخته‌اید.