Регресия — е бъг, който се появява след въвеждане на промени в кода, въпреки че същата функционалност е работила коректно преди това. Регресията означава, че новата промяна „е счупила“ това, което вече е било написано и тествано по-рано. Това е един от най-честите и опасни проблеми в разработката: поправяйки един бъг, разработчикът може незабелязано да счупи три други функции. Според Capers Jones Software Engineering 2023, средната плътност на регресионните бъгове е 1–3 на всеки 100 променени реда код. Разглеждаме причините за регресиите, методите за откриването им и стратегиите за предотвратяване.
Основни точки
Регресия — е ситуация, при която функционалност, работеща в предишната версия, спира да работи след въвеждане на промени. Промяната може да бъде всякаква: поправка на бъг, добавяне на нова функционалност, рефакториране, обновяване на библиотека или дори промяна на конфигурация. Регресията е основният враг на стабилността: всяка промяна рискува да счупи нещо, което вече е било проверено и пуснато.
Терминът идва от тестването: регресионно тестване — е повторно изпълнение на съществуващи тестове след всяка промяна. Ако тест, който преди е минавал, се провали — значи е настъпила регресия. В по-широк смисъл регресията не е само провал на тест, но и всяко влошаване на поведението, забелязано от потребителя или QA. Според Tricentis State of Testing 2023, регресиите съставляват 35–45% от всички бъгове, открити в продукция.
Регресията се различава от обикновения бъг по времеви контекст: бъгът е могъл да съществува винаги, докато регресията винаги е резултат от промяна. Това е важна разлика, защото търсенето на причината за регресия започва с анализ на промените: какво се е променило между „работеше“ и „спря да работи“. Git bisect — стандартният инструмент за намиране на commit, причинил регресия.
Локална регресия — промяна в модул A чупи функционалност в същия модул A. Пример: разработчик пренаписва функция за сортиране и тя спира да обработва правилно празен масив. Локалната регресия е най-лесна за откриване и поправяне, защото причината и следствието са близо едно до друго.
Отдалечена регресия — промяна в модул A чупи функционалност в модул B, който не е пряко свързан чрез код, но е свързан чрез данни или време. Пример: промяна на схемата на базата данни в модул „Потребители“ чупи отчет в модул „Аналитика“, който използва същата таблица. Отдалечените регресии са най-коварните: разработчикът не подозира, че неговата промяна ще засегне друг модул.
Регресия на странични ефекти — промяна на страничен ефект (логване, кеширане, изпращане на уведомления) чупи очакваното поведение. Пример: разработчик добави кеширане за ускоряване, но поради остарял кеш потребителите виждат остарели данни. Регресиите на странични ефекти са трудни за улавяне с автоматични тестове, защото страничните ефекти често не са покрити от тестове.
Производителна регресия — кодът продължава да работи коректно функционално, но по-бавно от преди. Пример: нов алгоритъм за криптиране дава същите резултати, но времето за изпълнение се е увеличило от 2 ms на 200 ms. Производителните регресии не се откриват от обикновени unit тестове — нужни са бенчмаркове и профилиране.
| Тип регресия | Пример | Метод за откриване |
|---|---|---|
| Локална | Счупено сортиране | Unit тестове |
| Отдалечена | Промяна на схема БД | Интеграционни тестове |
| Страничен ефект | Остарял кеш | E2E тестове |
| Производителна | Замъдняване на отговор | Бенчмаркове |
Първа причина — свързаност на кода (coupling). Колкото по-силно модулите зависят един от друг, толкова по-голяма е вероятността промяна в един да причини регресия в друг. Класически анти-патерни: God Object (обект, който прави всичко), Shotgun Surgery (промяна на едно изисква корекции на десетки места), Circular Dependency. Намаляването на coupling — задача на архитектурата: SOLID принципи, Dependency Injection, хексагонална архитектура.
Втора причина — липса на тестове за променяната функционалност. Ако кодът не е покрит с тестове, разработчикът научава за регресията едва от QA или потребителите. Според Google Testing Blog, проектите с покритие на тестовете >75% имат 5 пъти по-малко регресии от проекти с покритие <25%. TDD (Test-Driven Development) гарантира, че тестовете са написани преди кода, а не „когато има време“.
Трета причина — човешки фактор. Разработчикът не знае за съществуването на съседна функционалност, не разбира всички зависимости или просто бърза. Причина — недостатъчно споделяне на знания за кодовата база. Решения: code review с участие на разработчици от други модули, pair programming, документиране на архитектурата. Bus factor на проекта е обратно пропорционален на броя документирани архитектурни решения.
Регресионно тестване — е процес на повторно изпълнение на съществуващи тестове след всяка промяна, за да се провери дали старата функционалност не е счупена. Това е единственият начин да се гарантира, че новата промяна не е нарушила работата на съществуващия код. Без регресионно тестване всяко пускане е лотария: разработчикът се надява, че нищо не е счупил, но не може да го потвърди.
Ръчното регресионно тестване — най-скъпият и неефективен подход. С разрастването на проекта броят на регресионните тестови сценарии расте линейно, а времето за ръчното им изпълнение — експоненциално. След 2–3 години разработка ръчното изпълнение на регресия може да отнеме 2–3 седмици, което прави честите пускания невъзможни. Единственият изход е автоматизацията.
Автоматизираното регресионно тестване се разделя на нива според пирамидата на тестването:
Според Google Testing Blog, оптималното съотношение е: 70% unit тестове, 20% интеграционни, 10% E2E. Отклонението от това съотношение намалява ефективността на регресионното тестване: излишъкът от E2E тестове забавя тръбопровода, липсата на unit тестове оставя микро-бъгове незабелязани.
Първа стратегия — Full Regression. Изпълняват се всички тестове на проекта. Най-надеждният, но и най-бавният подход. Приложим за малки проекти (до 10 000 теста, време за изпълнение <30 минути). За големи проекти пълната регресия може да отнеме часове, което прави CI/CD тръбопровода непрактичен.
Втора стратегия — Selective Regression. Изпълняват се само тестове, свързани с променения код. За определяне на връзките се използва граф на зависимостите на кода. Инструменти: Bazel (Google), Nx (JavaScript), sbt (Scala). Selective regression спестява 60–80% от времето за изпълнение, но изисква точно изграждане на графа на зависимостите — грешките водят до пропуснати регресии.
Трета стратегия — Prioritized Regression. Всички тестове се класират по приоритет: critical path (най-важните потребителски сценарии), high risk (код с история на бъгове), changed code (код, засегнат от промяната). Първо се изпълняват най-приоритетните тестове — ако минат, разработчикът получава бърза обратна връзка. Изпълнение с времеви лимит: за 10 минути се проверяват критичните тестове, останалите — на заден план.
Първата и най-важна стъпка — култура на писане на тестове. Всяка промяна трябва да бъде придружена от тест, който проверява, че промяната работи, и тест, който проверява, че нищо не е счупено. TDD (Test-Driven Development) дава най-добри резултати: разработчикът първо пише тест, който се проваля, после код, който го преминава. Това гарантира, че тестът съществува преди кода.
Втора стъпка — CI/CD тръбопровод със задължително изпълнение на тестове. Pull request не може да бъде обединен, докато всички тестове не преминат. Тестовете не могат да бъдат “прескочени” поради спешност — спешните промени преминават през ускорен, но задължителен набор от тестове. Според Google DevOps Research, екипите със задължителен CI/CD имат 3 пъти по-малко регресии в продукция.
Трета стъпка — мониторинг в продукция. Дори най-добрите тестове не гарантират 100% защита от регресии. Инструментите за observability (Sentry, Datadog, New Relic) трябва да следят ключови метрики след всяко внедряване: процент грешки, латентност, пропускателна способност. Автоматично връщане (rollback) при превишаване на прагове — предпазна мрежа, ако регресията все пак попадне в продукция.
Четвърта стъпка — code review с регресионно мислене. Рецензентът трябва да зададе въпроса: “Кои други модули могат да бъдат счупени от тази промяна?”. Не е достатъчно да проверим, че кодът е коректен — трябва да проверим, че няма да наруши съседната функционалност. Контролният списък за code review трябва да включва точка “проверка за регресии в съседни модули”.
Често задавани въпроси
Регресията — е бъг, който не е съществувал преди. Обикновеният бъг е можел да съществува от момента на създаване на функционалността. Регресията винаги е свързана с конкретна промяна — това позволява използването на git bisect за намиране на причината.
Използвайте git bisect: посочете commit, където всичко работеше, и commit, където се счупи. Git ще извърши двоично търсене в историята и ще намери commit, причинил регресията. Това работи дори за големи проекти с хиляди commits.
Няма точно число, но има емпирично правило: покритието на ключовите потребителски потоци трябва да бъде 100%, покритието на всички функции — поне 70%. Качеството е по-важно от количеството: тест, който проверява edge case, струва повече от десет теста на happy path.
Да, и това се нарича infrastructure regression. Обновяване на операционната система, версията на базата данни, SSL сертификата или конфигурацията на уеб сървъра може да счупи работещ код. IaC (Infrastructure as Code) и тестването на инфраструктура (Test Kitchen, Terratest) помагат за улавяне на такива регресии.
Започнете с един критичен потребителски поток. Напишете автоматичен тест за най-важния сценарий (влизане, подаване на поръчка). Покажете на демо как тестът хваща регресия. Когато екипът види ползата — въвеждайте тестването постепенно, разширявайки покритието.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също