Български

Овладейте разработката, управлявана от тестове (TDD) в JavaScript. Това ръководство обхваща цикъла „Червено-Зелено-Рефакториране“, практическо приложение с Jest и добри практики.

Разработка, управлявана от тестове, в JavaScript: Цялостно ръководство за разработчици от цял свят

Представете си следния сценарий: поставена ви е задача да промените критична част от кода в голяма, наследена система. Изпитвате чувство на страх. Дали вашата промяна няма да счупи нещо друго? Как можете да сте сигурни, че системата все още работи според очакванията? Този страх от промяна е често срещан проблем в разработката на софтуер, който често води до бавен напредък и крехки приложения. Но какво, ако имаше начин да се създава софтуер с увереност, изграждайки предпазна мрежа, която улавя грешките, преди те изобщо да достигнат до продукционна среда? Това е обещанието на разработката, управлявана от тестове (Test-Driven Development - TDD).

TDD не е просто техника за тестване; това е дисциплиниран подход към дизайна и разработката на софтуер. Той обръща традиционния модел „пиши код, след това тествай“. С TDD вие пишете тест, който се проваля преди да напишете продукционния код, който да го накара да премине. Това просто обръщане има дълбоки последици за качеството, дизайна и поддръжката на кода. Това ръководство ще предостави цялостен, практически поглед върху внедряването на TDD в JavaScript, предназначен за глобална аудитория от професионални разработчици.

Какво е разработка, управлявана от тестове (TDD)?

В своята същност, разработката, управлявана от тестове, е процес на разработка, който разчита на повтарянето на много кратък цикъл. Вместо да пишете функционалности и след това да ги тествате, TDD настоява тестът да бъде написан първо. Този тест неизбежно ще се провали, защото функционалността все още не съществува. Работата на разработчика тогава е да напише възможно най-простия код, за да накара този конкретен тест да премине. След като премине, кодът се почиства и подобрява. Този основен цикъл е известен като цикъл „Червено-Зелено-Рефакториране“ (Red-Green-Refactor).

Ритъмът на TDD: Червено-Зелено-Рефакториране

Този тристъпков цикъл е сърцето на TDD. Разбирането и практикуването на този ритъм е фундаментално за овладяването на техниката.

След като цикълът приключи за една малка част от функционалността, вие започвате отново с нов провалящ се тест за следващата част.

Трите закона на TDD

Робърт С. Мартин (често известен като „Чичо Боб“), ключова фигура в движението за гъвкав софтуер (Agile), дефинира три прости правила, които кодифицират дисциплината на TDD:

  1. Нямате право да пишете продукционен код, освен ако не е, за да накарате провалящ се единичен тест да премине.
  2. Нямате право да пишете повече от един единичен тест, отколкото е достатъчно, за да се провали; и грешките при компилация са провали.
  3. Нямате право да пишете повече продукционен код, отколкото е достатъчно, за да премине единственият провалящ се единичен тест.

Следването на тези закони ви принуждава да влезете в цикъла „Червено-Зелено-Рефакториране“ и гарантира, че 100% от вашия продукционен код е написан, за да удовлетвори конкретно, тествано изискване.

Защо да приемете TDD? Бизнес аргументите в глобален мащаб

Макар TDD да предлага огромни ползи за отделните разработчици, истинската му сила се реализира на ниво екип и бизнес, особено в глобално разпределени среди.

Настройване на вашата среда за TDD с JavaScript

За да започнете с TDD в JavaScript, са ви нужни няколко инструмента. Модерната екосистема на JavaScript предлага отлични възможности.

Основни компоненти на стека за тестване

Заради своята простота и „всичко в едно“ природа, ще използваме Jest за нашите примери. Той е отличен избор за екипи, които търсят изживяване с „нулева конфигурация“.

Настройка стъпка по стъпка с Jest

Нека да настроим нов проект за TDD.

1. Инициализирайте вашия проект: Отворете терминала си и създайте нова директория за проекта.

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

2. Инсталирайте Jest: Добавете Jest към проекта си като зависимост за разработка (development dependency).

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);
  });
});

Сега стартирайте test watcher-а от вашия терминал:

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.

🔴 ЧЕРВЕНО: Напишете асинхронен тест, който моква API извикване

Първо, трябва да преструктурираме нашия конвертор. Сега той ще трябва да бъде клас, който можем да инстанцираме, може би с API клиент. Също така ще трябва да мокнем `fetch` API. 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` тогава ще използва този fetcher. Това следва Принципа на единствената отговорност (Single Responsibility Principle) и прави и двата класа по-лесни за тестване и поддръжка. TDD ни насочва към този по-чист дизайн.

Често срещани модели и анти-модели в TDD

Докато практикувате TDD, ще откриете модели, които работят добре, и анти-модели, които създават търкания.

Добри модели за следване

Анти-модели, които да избягвате

TDD в по-широкия жизнен цикъл на разработката

TDD не съществува във вакуум. Той се интегрира прекрасно със съвременните Agile и DevOps практики, особено за глобални екипи.

Заключение: Вашето пътешествие с TDD

Разработката, управлявана от тестове, е повече от стратегия за тестване – това е промяна на парадигмата в начина, по който подхождаме към разработката на софтуер. Тя насърчава култура на качество, увереност и сътрудничество. Цикълът „Червено-Зелено-Рефакториране“ осигурява стабилен ритъм, който ви води към чист, здрав и лесен за поддръжка код. Резултатният тестов пакет се превръща в предпазна мрежа, която защитава екипа ви от регресии, и в жива документация, която въвежда новите членове.

Кривата на учене може да изглежда стръмна, а първоначалното темпо може да изглежда по-бавно. Но дългосрочните дивиденти под формата на намалено време за отстраняване на грешки, подобрен дизайн на софтуера и повишена увереност на разработчиците са неизмерими. Пътуването към овладяването на TDD е пътуване на дисциплина и практика.

Започнете днес. Изберете една малка, некритична функционалност в следващия си проект и се посветете на процеса. Напишете теста първо. Гледайте го как се проваля. Накарайте го да премине. И тогава, най-важното, рефакторирайте. Изпитайте увереността, която идва от зелен тестов пакет, и скоро ще се чудите как изобщо сте създавали софтуер по друг начин.