<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
ТЕСТУВАННЯ · SENIOR

Як покрити тестами легасі-код, у якому тестів немає взагалі?

Спершу зафіксувати те, що код робить зараз, а не те, що він мав би робити: characterization-тести (Майкл Фезерс) або golden master на рівні HTTP чи консольної команди. Далі знайти шви (seams), через які можна підставити залежності, мінімальними безпечними рефакторингами винести логіку в класи, що тестуються, і лише тоді міняти поведінку. Нову логіку пишуть поруч і з тестами (sprout method, wrap method), а не всередині старого моноліту.

Вам дали клас на 3000 рядків без жодного тесту, і в ньому треба змінити розрахунок комісії. З чого почнете?
Що таке characterization test і чим він відрізняється від звичайного?
Як тестувати код, що всередині робить `new PDO()` і `date()`?
Ви знайшли в легасі явний баг, а тест-характеристика фіксує саме цю поведінку. Що робити?
легасі characterization tests golden master snapshot seams рефакторинг

Майкл Фезерс (Michael Feathers) у книжці «Working Effectively with Legacy Code» (2004) визначив легасі просто: це код без тестів. Біда такого коду в тому, що будь-яка зміна стає ставкою: ніхто не знає, що саме зламається. Тому першим кроком іде сітка безпеки з тестів, що фіксують поточну поведінку, і лише потім рефакторинг чи нова фіча.

Такі тести Фезерс назвав characterization tests. Вони описують те, що код робить зараз, а не те, що він мав би робити. Техніка механічна: викликати код з реалістичними даними, поставити свідомо неправильне очікування, запустити й переписати очікування з повідомлення про падіння. Кілька десятків таких випадків, включно з нулями, порожніми рядками й рідкісними статусами, дають набір, який кричить при будь-якій зміні поведінки. Якщо серед результатів трапився явний баг, його не виправляють мовчки: позначають, узгоджують з бізнесом і виправляють окремою зміною, бо на нього вже може спиратися звіт чи інша система.

Коли до логіки всередині не дістатися, бо клас сам створює PDO, кличе date() і статичні хелпери, фіксують зовнішню межу: HTTP-відповідь, вивід консольної команди, згенерований файл. Це golden master, або snapshot. У Pest є toMatchSnapshot(), що зберігає еталон при першому запуску й порівнює з ним далі. Нестабільні частини (дати, CSRF-токени, автоінкрементні id) нормалізують або зупиняють через travelTo(), інакше еталон не збігатиметься ніколи.

Далі шукають шви (seams): місця, де поведінку можна змінити без редагування коду в цьому місці. Параметр конструктора замість new всередині, перевизначений метод у тестовому підкласі, підміна біндінгу в контейнері Laravel. Перші зміни роблять маленькими й безпечними, бажано автоматичними рефакторингами IDE: витягти метод, передати залежність через конструктор, загорнути date() у Clock. Під захистом characterization-тестів це вже не ставка.

Нову поведінку пишуть не всередині старого класу, а поруч. Sprout method або sprout class: логіка живе в новому коді з повноцінними unit-тестами, а в легасі зʼявляється один рядок виклику. Wrap method: старий метод перейменовують, новий з тим самим імʼям викликає його й додає поведінку. Так нетестована частина поступово стискається, а характеризаційні тести, що виконали свою роль, замінюють на звичайні тести поведінки, які описують правила, а не історичний вивід.

// Легасі: розрахунок комісії всередині старого сервісу, без тестів.
// Спершу фіксуємо, що він робить ЗАРАЗ, на реальних вхідних даних.
it('keeps the current commission rules', function (int $amount, string $status, int $expected) {
    $service = new LegacyBillingService();   // конструктор без залежностей, пощастило

    expect($service->commission($amount, $status))->toBe($expected);
})->with([
    // Очікування взяті з виводу самого коду, а не з вимог.
    'звичайний платіж' => [10_000, 'paid', 250],
    'нуль' => [0, 'paid', 0],
    'повернення' => [10_000, 'refunded', 500],   // підозріло: комісія вдвічі? TICKET-123
]);

// Коли всередину не зайти, фіксуємо зовнішній вивід цілком (golden master).
it('renders the same invoice page', function () {
    $this->travelTo('2026-10-01 10:00');      // дата в рахунку стабільна

    $html = $this->get('/invoices/42')->assertOk()->getContent();
    // Токени й id, що змінюються між запусками, нормалізуємо.
    $html = preg_replace('/name="_token" value="[^"]+"/', 'name="_token" value="X"', $html);

    expect($html)->toMatchSnapshot();
});
Порядок дій: зафіксувати поточну поведінку, отримати шов, зробити зміну, і саме в такій послідовності, без «спершу перепишу, потім протестую».
Що characterization-тест описує фактичну поведінку, навіть дивну, і очікуване значення беруть з виводу самого коду, а не з вимог.
Golden master або snapshot на зовнішній межі (HTTP-відповідь, вивід команди, згенерований файл), коли всередину зайти неможливо, і нормалізація нестабільних частин (дати, id, токени).
Поняття шва з книжки «Working Effectively with Legacy Code»: місце, де поведінку можна змінити, не редагуючи код у ньому (параметр конструктора, перевизначений метод, підміна в контейнері).
Техніки безпечної зміни: sprout method/class для нової логіки, wrap method, витяг методу з перевіреним рефакторингом в IDE, і дисципліна маленьких комітів.
Починати з переписування класу «по-нормальному», а тести писати після: без зафіксованої поведінки неможливо довести, що нічого не зламано.
Виправляти знайдений баг одразу в characterization-тесті, не узгодивши з бізнесом: хтось може покладатися на цю поведінку, як на функцію.
Робити snapshot без нормалізації: у відповіді є дата, CSRF-токен чи автоінкремент, і тест падає на кожному запуску.
Мокати все підряд в обʼєкті на 3000 рядків, щоб він хоч якось створився: тест знає кожну деталь реалізації і стає на заваді рефакторингу, заради якого його писали.
Тягнути цілий клас у tests/Unit через Reflection до приватних методів замість того, щоб винести шматок логіки в окремий клас.
ПОРАДА

Відповідайте як план на перший день: «зафіксую поточну поведінку тестом на рівні HTTP чи команди з кількома реальними вхідними даними, потім знайду шов для дати й бази, винесу розрахунок комісії в окремий клас з unit-тестами, і тільки тоді зміню правило». Слова «поточна поведінка, навіть якщо вона неправильна» показують досвід роботи з легасі.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

У легасі без тестів немає іншого способу довести, що рефакторинг нічого не зламав, ніж зафіксувати фактичну поведінку. Characterization-тест бере очікування з виводу самого коду. Баг, який він виявив, узгоджують і виправляють окремою зміною, бо на нього вже можуть покладатися інші частини системи.