رگرسیون — چیست، چرا رخ می‌دهد و چگونه تست کنیم

نویسنده: IT Sectr منتشر شده: 2026-07-30 زمان مطالعه: 7 دقیقه

رگرسیون — باگی است که پس از ایجاد تغییرات در کد ظاهر می‌شود، با وجود اینکه همان قابلیت قبلاً به درستی کار می‌کرد. رگرسیون به این معنی است که تغییر جدید «آنچه را که قبلاً نوشته و تست شده بود شکسته است». این یکی از رایج‌ترین و خطرناک‌ترین مشکلات در توسعه است: با رفع یک باگ، برنامه‌نویس ممکن است ناآگاهانه سه عملکرد دیگر را بشکند. طبق Capers Jones Software Engineering 2023، تراکم متوسط باگ‌های رگرسیون ۱–۳ به ازای هر ۱۰۰ خط کد تغییر یافته است. علل رگرسیون، روش‌های تشخیص و استراتژی‌های پیشگیری را بررسی می‌کنیم.

نکات کلیدی

  • رگرسیون — باگی که پس از اعمال تغییرات در کد قبلاً کارکرده ایجاد می‌شود
  • علت اصلی — عوارض جانبی تغییرات: کد با وابستگی‌های ضمنی متصل است
  • تست‌های واحد و تست رگرسیون — ابزارهای اصلی تشخیص رگرسیون
  • تست رگرسیون دستی مقیاس‌پذیر نیست — خودکارسازی لازم است
  • خط لوله CI/CD با تست‌های خودکار رگرسیون‌ها را قبل از رسیدن به تولید می‌گیرد

رگرسیون در توسعه چیست

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

این اصطلاح از تست‌گیری می‌آید: تست رگرسیون — اجرای مجدد تست‌های موجود پس از هر تغییر است. اگر تستی که قبلاً موفق بود شکست بخورد — یعنی رگرسیون رخ داده است. به معنای گسترده‌تر، رگرسیون نه تنها شکست تست، بلکه هرگونه بدتر شدن رفتار مشاهده شده توسط کاربر یا QA است. طبق Tricentis State of Testing 2023، رگرسیون‌ها ۳۵–۴۵٪ از کل باگ‌های یافت شده در تولید را تشکیل می‌دهند.

رگرسیون با زمینه زمانی از باگ معمولی متفاوت است: باگ ممکن است همیشه وجود داشته باشد، اما رگرسیون همیشه نتیجه یک تغییر است. این تفاوت مهمی است، زیرا جستجوی علت رگرسیون با تحلیل تغییرات آغاز می‌شود: چه چیزی بین «کار می‌کرد» و «از کار افتاد» تغییر کرده است. Git bisect — ابزار استاندارد برای یافتن کامیتی که باعث رگرسیون شده است.

انواع رگرسیون و مثال‌ها

رگرسیون محلی — تغییر در ماژول A عملکرد را در همان ماژول A خراب می‌کند. مثال: برنامه‌نویس تابع مرتب‌سازی را بازنویسی می‌کند و آن تابع دیگر آرایه خالی را به درستی پردازش نمی‌کند. رگرسیون محلی ساده‌ترین نوع برای تشخیص و رفع است، زیرا علت و معلول نزدیک به هم هستند.

رگرسیون از راه دور — تغییر در ماژول A عملکرد را در ماژول B خراب می‌کند که مستقیماً با کد مرتبط نیست، اما با داده یا زمان مرتبط است. مثال: تغییر طرح پایگاه داده در ماژول «کاربران» گزارشی را در ماژول «تحلیل» که از همان جدول استفاده می‌کند خراب می‌کند. رگرسیون‌های از راه دور موذیانه‌ترین هستند: برنامه‌نویس گمان نمی‌کند که تغییرش بر ماژول دیگر تأثیر بگذارد.

رگرسیون عوارض جانبی — تغییر عارضه جانبی (ورود به سیستم، ذخیره‌سازی موقت، ارسال اعلان) رفتار مورد انتظار را خراب می‌کند. مثال: برنامه‌نویس برای سرعت بخشیدن به کار، ذخیره‌سازی موقت اضافه کرد، اما به دلیل کش قدیمی، کاربران داده‌های منسوخ را می‌بینند. رگرسیون‌های عوارض جانبی با تست‌های خودکار به سختی گرفته می‌شوند، زیرا عوارض جانبی اغلب تحت پوشش تست نیستند.

رگرسیون عملکرد — کد همچنان از نظر عملکردی درست کار می‌کند، اما کندتر از قبل است. مثال: الگوریتم رمزگذاری جدید همان نتایج را می‌دهد، اما زمان اجرا از ۲ میلی‌ثانیه به ۲۰۰ میلی‌ثانیه افزایش یافته است. رگرسیون‌های عملکرد با تست‌های واحد معمولی تشخیص داده نمی‌شوند — نیاز به بنچمارک و پروفایل‌گیری دارند.

نوع رگرسیونمثالروش تشخیص
محلیمرتب‌سازی خرابتست‌های واحد
از راه دورتغییر طرح پایگاه دادهتست‌های یکپارچه‌سازی
عوارض جانبیکش قدیمیتست‌های E2E
عملکردکندی پاسخبنچمارک‌ها

چرا رگرسیون رخ می‌دهد

علت اول — اتصال کد (coupling). هرچه ماژول‌ها بیشتر به یکدیگر وابسته باشند، احتمال اینکه تغییر در یکی باعث رگرسیون در دیگری شود بیشتر است. الگوهای ضد کلاسیک: God Object (شیء که همه کار انجام می‌دهد)، Shotgun Surgery (تغییر یک مورد نیازمند اصلاح در ده‌ها مکان)، Circular Dependency. کاهش coupling — وظیفه معماری است: اصول SOLID، تزریق وابستگی، معماری شش‌ضلعی.

علت دوم — عدم وجود تست برای قابلیت در حال تغییر. اگر کد تحت پوشش تست نباشد، برنامه‌نویس فقط از طریق QA یا کاربران از رگرسیون مطلع می‌شود. طبق Google Testing Blog، پروژه‌هایی با پوشش تست >۷۵٪ پنج برابر کمتر از پروژه‌هایی با پوشش <۲۵٪ رگرسیون دارند. TDD (توسعه تست‌محور) تضمین می‌کند که تست‌ها قبل از کد نوشته می‌شوند، نه «وقتی وقت شد».

علت سوم — عامل انسانی. برنامه‌نویس از وجود قابلیت مجاور اطلاع ندارد، تمام وابستگی‌ها را درک نمی‌کند یا صرفاً عجله دارد. علت — اشتراک‌گذاری ناکافی دانش پایگاه کد. راه‌حل‌ها: بررسی کد با مشارکت برنامه‌نویسان ماژول‌های دیگر، برنامه‌نویسی جفتی، مستندسازی معماری. عامل اتوبوس پروژه با تعداد تصمیمات معماری مستند شده رابطه معکوس دارد.

تست رگرسیون و نقش آن

تست رگرسیون — فرآیند اجرای مجدد تست‌های موجود پس از هر تغییر برای بررسی اینکه قابلیت قدیمی خراب نشده است. این تنها راه تضمین این است که تغییر جدید عملکرد کد موجود را مختل نکرده است. بدون تست رگرسیون، هر انتشار یک قرعه‌کشی است: برنامه‌نویس امیدوار است چیزی را خراب نکرده، اما نمی‌تواند آن را تأیید کند.

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

تست رگرسیون خودکار بر اساس هرم تست به سطوح تقسیم می‌شود:

  • تست‌های واحد — سریع، ایزوله، توابع و متدهای جداگانه را پوشش می‌دهند
  • تست‌های یکپارچه‌سازی — تعامل بین ماژول‌ها، پایگاه داده، سرویس‌های خارجی را بررسی می‌کنند
  • تست‌های E2E — سناریوهای کامل کاربر را از طریق UI یا API بررسی می‌کنند
  • تست‌های snapshot — خروجی فعلی کامپوننت را با مرجع مقایسه می‌کنند

طبق Google Testing Blog، نسبت بهینه: ۷۰٪ تست واحد، ۲۰٪ یکپارچه‌سازی، ۱۰٪ E2E. انحراف از این نسبت اثربخشی تست رگرسیون را کاهش می‌دهد: فراوانی تست‌های E2E خط لوله را کند می‌کند، کمبود تست‌های واحد ریزباگ‌ها را نادیده می‌گذارد.

استراتژی‌های خودکارسازی تست رگرسیون

استراتژی اول — Full Regression. تمام تست‌های پروژه اجرا می‌شوند. قابل اعتمادترین، اما کندترین رویکرد. مناسب برای پروژه‌های کوچک (تا ۱۰٬۰۰۰ تست، زمان اجرا <۳۰ دقیقه). برای پروژه‌های بزرگ، رگرسیون کامل ممکن است ساعت‌ها طول بکشد که خط لوله CI/CD را غیرعملی می‌کند.

استراتژی دوم — Selective Regression. فقط تست‌های مرتبط با کد تغییر یافته اجرا می‌شوند. برای تعیین ارتباط از گراف وابستگی کد استفاده می‌شود. ابزارها: Bazel (Google)، Nx (JavaScript)، sbt (Scala). Selective regression ۶۰–۸۰٪ زمان اجرا را صرفه‌جویی می‌کند، اما نیاز به ساخت دقیق گراف وابستگی دارد — خطاها منجر به رگرسیون‌های از دست رفته می‌شوند.

استراتژی سوم — Prioritized Regression. همه تست‌ها بر اساس اولویت رتبه‌بندی می‌شوند: critical path (مهم‌ترین سناریوهای کاربر)، high risk (کد با سابقه باگ)، changed code (کد تحت تأثیر تغییر). ابتدا با اولویت‌ترین تست‌ها اجرا می‌شوند — اگر موفق شوند، برنامه‌نویس بازخورد سریع دریافت می‌کند. اجرای زمان‌بندی شده: در ۱۰ دقیقه تست‌های بحرانی بررسی می‌شوند، بقیه — در پس‌زمینه.

چگونه از رگرسیون در پروژه جلوگیری کنیم

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

گام دوم — خط لوله CI/CD با اجرای اجباری تست. Pull request نمی‌تواند ادغام شود تا زمانی که همه تست‌ها موفق نشوند. به دلیل فوریت نمی‌توان تست‌ها را «رد کرد» — تغییرات فوری از یک مجموعه تست تسریع‌شده اما اجباری عبور می‌کنند. طبق Google DevOps Research، تیم‌های دارای CI/CD اجباری سه برابر رگرسیون کمتری در تولید دارند.

گام سوم — نظارت در تولید. حتی بهترین تست‌ها ۱۰۰٪ محافظت در برابر رگرسیون را تضمین نمی‌کنند. ابزارهای مشاهده‌پذیری (Sentry, Datadog, New Relic) باید معیارهای کلیدی را پس از هر استقرار ردیابی کنند: نرخ خطا، تأخیر، توان عملیاتی. بازگشت خودکار (rollback) هنگام فراتر رفتن از آستانه‌ها — بالشتک ایمنی اگر رگرسیون به تولید راه یابد.

گام چهارم — بررسی کد با تفکر رگرسیونی. بررسی‌کننده باید سوال بپرسد: «کدام ماژول‌های دیگر ممکن است از این تغییر خراب شوند؟». کافی نیست بررسی کنیم کد درست است — باید بررسی کنیم که قابلیت مجاور را خراب نمی‌کند. فهرست بررسی برای code review باید شامل بند «بررسی رگرسیون در ماژول‌های مجاور» باشد.

سوالات متداول

رگرسیون چه تفاوتی با باگ معمولی دارد؟

رگرسیون — باگی است که قبلاً وجود نداشت. باگ معمولی ممکن است از لحظه ایجاد ویژگی وجود داشته باشد. رگرسیون همیشه به یک تغییر خاص مرتبط است — این امکان استفاده از git bisect برای یافتن علت را فراهم می‌کند.

چگونه سریع علت رگرسیون را پیدا کنیم؟

از git bisect استفاده کنید: کامیتی را که همه چیز کار می‌کرد و کامیتی را که خراب شد مشخص کنید. Git جستجوی دودویی در تاریخچه انجام می‌دهد و کامیتی را که باعث رگرسیون شده پیدا می‌کند. این حتی در پروژه‌های بزرگ با هزاران کامیت کار می‌کند.

چه تعداد تست برای محافظت در برابر رگرسیون نیاز است؟

عدد دقیقی وجود ندارد، اما یک قانون تجربی وجود دارد: پوشش جریان‌های کلیدی کاربر باید ۱۰۰٪ باشد، پوشش همه ویژگی‌ها — حداقل ۷۰٪. کیفیت مهم‌تر از کمیت است: تستی که edge case را بررسی می‌کند از ده تست در happy path ارزشمندتر است.

آیا رگرسیون می‌تواند نه توسط کد، بلکه توسط زیرساخت ایجاد شود؟

بله، و به آن infrastructure regression می‌گویند. به‌روزرسانی سیستم عامل، نسخه پایگاه داده، گواهی SSL یا پیکربندی وب‌سرور ممکن است کد در حال کار را خراب کند. IaC (زیرساخت به عنوان کد) و تست زیرساخت (Test Kitchen, Terratest) به تشخیص چنین رگرسیون‌هایی کمک می‌کنند.

چگونه تیم را به نوشتن تست‌های رگرسیون متقاعد کنیم اگر قبلاً هرگز ننوشته‌اند؟

از یک جریان بحرانی کاربر شروع کنید. برای مهم‌ترین سناریو (ورود، ثبت سفارش) تست خودکار بنویسید. در دمو نشان دهید که تست چگونه رگرسیون را می‌گیرد. وقتی تیم فایده را دید — تست را تدریجی پیاده‌سازی کنید و پوشش را گسترش دهید.

خلاصه

  • رگرسیون — باگی که پس از تغییر کد قبلاً کارکرده ایجاد می‌شود
  • چهار نوع رگرسیون: محلی, از راه دور, عوارض جانبی و عملکرد
  • علت اصلی — اتصال کد, فقدان تست و عامل انسانی
  • تست رگرسیون — فرآیند اجباری برای حفظ پایداری
  • خودکارسازی تست‌های رگرسیون از طریق هرم تست (۷۰/۲۰/۱۰)
  • CI/CD با اجرای اجباری تست‌ها رگرسیون‌ها را در ورودی مسدود می‌کند
  • Git bisect — ابزار استاندارد برای یافتن کامیت ایجادکننده رگرسیون

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید