«تستها را روی ۱۲ Worker اجرا کردیم؛ پس چرا Regression هنوز ۷۸ دقیقه طول میکشد؟» پاسخ معمولاً کمبود Worker نیست. شاید یک فایل سنگین Critical Path ساخته، چهار Shard از نظر تعداد مساوی اما از نظر زمان نامتوازناند، Global Setup سریال است، تستها روی یک حساب یا PSP Sandbox قفل میشوند، Retry زمان و اعتماد را میبلعد، یا مرحلهٔ ادغام گزارش بعد از پایان تستها گلوگاه تازهای ساخته است.
بهینهسازی تست رگرسیون یعنی کاهش «زمان رسیدن به تصمیم قابلاعتماد» با حفظ قرارداد شواهد؛ نه حذف خاموش تست، زیادکردن کورکورانهٔ Parallelism یا سبزکردن Pipeline با Retry. در این راهنمای عملی، از Baseline و Critical Path تا Sharding، Load Balancing، Isolation، Cache، Flake Tax و Capacity Experiment پیش میرویم. یک آزمایش قطعی و قابلبازتولید نیز نشان میدهد چرا چهار بخش هماندازه از نظر تعداد الزاماً چهار بخش همزمان نیستند.
بهینهسازی Regression Suite دقیقاً چه مسئلهای را حل میکند؟
Regression Suite مجموعهای از تستهاست که برای کشف اثر ناخواستهٔ تغییر روی رفتار موجود اجرا میشود. مسئلهٔ این مقاله «چه تستهایی باید وجود داشته باشند؟» یا «برای این Commit کدام تست انتخاب شود؟» نیست؛ مسئله، عملکرد سامانهای است که تستهای از قبل تصمیمگیریشده را اجرا و نتیجهٔ قابل اتکا تولید میکند.
هدف، Runtime کمتر به هر قیمت نیست
هدف عملی را میتوان اینگونه نوشت:
کاهش زمان تا Evidence قابل اقدام
با حفظ Coverage Contract، Oracle، استقلال، قابلیت بازتولید و کاملبودن گزارش
در سقف هزینه و ظرفیت مورد توافق
اگر ۲۰ دقیقه از زمان کم شود اما تست پرداخت حذف شود، Failure با Retry پنهان بماند یا نتیجهٔ یک Shard در گزارش نهایی گم شود، Suite سریعتر نشده است؛ فقط Evidence ناقص را زودتر تحویل میدهد.
مرز این راهنما با مقالههای نزدیک
- برای تعیین نسبت Unit، Integration و E2E و اینکه چه آزمونهایی در Portfolio لازماند، راهنمای عملی Test Pyramid را ببینید.
- برای انتخاب مبتنی بر تغییر و ریسک، Budget و Safe Fallback، به پیادهسازی تست مبتنی بر ریسک در CI مراجعه کنید.
- برای طراحی Laneها، Gateها و سیاست شکست کل Pipeline، معماری Continuous Testing در CI/CD مالک آن Intent است.
- اگر Suite پر از تست شکسته، کمارزش و غیرقابلاعتماد است، ابتدا برنامهٔ نجات اتوماسیون تست را اجرا کنید.
اینجا فرض میکنیم قرارداد پوشش و Gate معلوم است و میخواهیم موتور اجرا را بدون پنهانکردن شواهد مهندسی کنیم.
چهار زمان را از یکدیگر جدا کنید
عبارت «تستها ۴۰ دقیقه طول کشیدند» مبهم است. چهار ساعت مختلف ممکن است پشت آن باشد و هرکدام درمان متفاوتی دارد.
۱. Queue Time
فاصلهٔ ثبت Job تا شروع واقعی روی Runner است. اگر Runtime خود تست ۱۸ دقیقه اما صف ۲۵ دقیقه باشد، بهینهکردن Locatorها مسئلهٔ اصلی را حل نمیکند. Queue باید برحسب Runner pool، ساعت روز، نوع Branch و Priority گزارش شود.
۲. Test Wall-clock Time
فاصلهٔ شروع اولین کار مرتبط تا پایان آخرین کار لازم است. این همان زمانی است که توسعهدهنده یا Gate واقعاً حس میکند. جمع Duration تستها با Wall-clock برابر نیست؛ زیرا بخشی موازی و بخشی سریال است.
۳. Time to First Actionable Failure
اولین Failure خام الزاماً قابل اقدام نیست. اگر Trace، Build ID، داده، Environment و Reproduction وجود ندارد یا شکست در Retry ناپدید میشود، تصمیم هنوز آماده نیست. «زمان تا اولین Evidence قابل اقدام» برای PRها اغلب از زمان پایان کامل Suite مهمتر است، اما نباید جای نتیجهٔ جامع Gate را بگیرد.
۴. Time to Trusted Decision
تا وقتی تمام Shardهای لازم تمام نشده، Blob/JUnitها ادغام نشدهاند، Missing Result بررسی نشده و سیاست Retry/Quarantine اعمال نشده است، تصمیم Pass/Fail معتبر نیست. این زمان، Merge و Validation گزارش را هم شامل میشود.
Baseline قابل دفاع بسازید
یک Screenshot از یک Run مبنا نیست. حداقل دو هفته یا تعداد کافی Run همنوع را با نسخهٔ ثابت تعریفها ثبت کنید. PR، Nightly، مرورگر، Runner size و Region را مخلوط نکنید. Runهای Cancelشده، Infra-failure و Product-failure باید برچسب جدا داشته باشند، نه اینکه بیصدا از داده حذف شوند.
Run Manifest حداقلی
{
"run_id": "reg-2026-08-12-1842",
"commit": "a4c91e7",
"build_id": "checkout-2841",
"suite_contract_version": "regression-v17",
"runner_image": "pw-1.58.2-node24-v3",
"environment": "staging-ir-2",
"shard": { "index": 2, "total": 4 },
"queue_started_at_utc": "2026-08-12T15:12:09Z",
"execution_started_at_utc": "2026-08-12T15:14:20Z",
"report_merged_at_utc": "2026-08-12T15:52:41Z",
"selection_manifest_sha256": "...",
"tests_expected": 184,
"tests_observed": 184
}
زمان را در UTC ذخیره و برای نمایش به Asia/Tehran تبدیل کنید. تغییر ساعت یا تقویم نمایشی نباید محاسبهٔ Duration را دستکاری کند.
فازها را جدا Instrument کنید
| فاز | پرسش تشخیصی | نمونهٔ Evidence |
|---|---|---|
| Queue | Job منتظر کدام Runner یا Tag بود؟ | created_at، started_at، runner_pool |
| Provision | ساخت Container/VM چقدر طول کشید؟ | image pull، pod scheduling |
| Dependency/Build | Cache hit واقعی بود؟ | cache_key، hit/miss، build hash |
| Global Setup | کدام کار فقط یک بار و کدام برای هر Shard تکرار شد؟ | migration، seed، login state |
| Execution | P50/P95 تست و Long Tail چیست؟ | test_id، attempt، duration |
| Contention | چه منبع مشترکی اشباع یا قفل شد؟ | DB pool، API ۴۲۹، lock wait |
| Retry | چند دقیقه برای Attempt اضافه مصرف شد؟ | initial/retry status، failure signature |
| Teardown | پاکسازی یا آپلود Trace گلوگاه است؟ | artifact bytes، upload duration |
| Merge/Validate | آیا همهٔ نتایج مورد انتظار حاضرند؟ | expected/observed IDs، merge status |
میانه کافی نیست
برای Wall-clock و Queue دستکم Median، P90/P95 و پراکندگی را ببینید. میانگین ممکن است پنج Run بسیار کند را پنهان کند. برای هر Test ID نیز Duration تاریخی، نرخ تغییر، Failure signature، Retry و منبع مصرفی را نگه دارید. Window باید Rolling اما Versioned باشد؛ تاریخچهٔ قبل از یک بازنویسی بزرگ تست نباید برای همیشه وزن Shard را خراب کند.
Critical Path و Lower Bound را پیدا کنید
در یک DAG اجرایی، Critical Path طولانیترین زنجیرهٔ وابستگی است که پایان Pipeline را تعیین میکند. اضافهکردن Worker نمیتواند مرحلهٔ سریال Migration→Seed→Deploy یا یک فایل ۵۵ دقیقهای غیرقابلتقسیم را کوتاهتر از طول خودش کند.
کران پایین ساده
T_wall ≥ T_queue + T_setup_serial
+ max(Σ duration / N_workers,
T_longest_nonparallel_chain,
T_slowest_constrained_resource)
+ T_merge
این یک مدل تشخیصی است، نه پیشبینی دقیق. Scheduling overhead، Cache miss، تفاوت ماشین، شبکه و Contention میتوانند زمان واقعی را بالاتر ببرند. اگر مجموع تست ۴۲۰ دقیقه و ۱۲ Worker است، ۳۵ دقیقه فقط یک کران حسابی است؛ نه SLA.
نشانههای Critical Path پنهان
- یک فایل بزرگ شامل چند Journey است و Runner آن را واحد تقسیم میبیند.
- تمام تستها پیش از شروع منتظر یک Global Login یا Database restore هستند.
- تستهای پرداخت روی یک Merchant، شماره موبایل یا Order مشترک قفل میشوند.
- چهار Job زود تمام میشوند اما یک Job مدت زیادی تنها میماند.
- Artifact upload یا Report merge بعد از اتمام Execution طولانی است.
- Worker پس از Failure بازنشانی میشود و Setup پرهزینه دوباره رخ میدهد.
در Playwright، تستها داخل Worker Process اجرا میشوند و Worker پس از Failure متوقف میشود؛ بنابراین هزینهٔ Setup و شکل گروهبندی فایلها روی زمان اثر دارد. مستندات رسمی Parallelism در Playwright این رفتار و Worker Index را توضیح میدهد.
اول Correctness و Isolation؛ بعد Parallelism
تستی که فقط در ترتیب خاص Pass میشود، آمادهٔ اجرای موازی نیست. موازیسازی Dependency پنهان را سریعتر آشکار میکند، اما اگر تیم آن را با Retry بپوشاند، Signal بدتر میشود.
قرارداد استقلال هر تست
- هر تست دادهٔ ورودی، User، Cart، Order و Idempotency Key اختصاصی دارد.
- تست برای شروع به اثر جانبی تست قبلی متکی نیست.
- پاکسازی، Idempotent است و شکست آن نتیجهٔ تست دیگر را آلوده نمیکند.
- Clock، Random seed و Locale در صورت اثرگذاری کنترل و ثبت میشوند.
- سرویس Remote محدود یا ناپایدار با Sandbox قرارداددار، Stub یا Test Double مناسب کنترل میشود.
- منابع انحصاری صریحاً Tag میشوند؛ Scheduling نباید قفل پنهان بسازد.
راهنمای رفع Non-determinism در تستها، نبود Isolation، رفتار Async، سرویس Remote، زمان و نشت منابع را از علتهای رایج میداند. Quarantine فقط آسیب را محدود میکند و جای Fix را نمیگیرد.
Namespace داده با Worker ID
import { test as base } from '@playwright/test';
export const test = base.extend<{ customerKey: string }>({
customerKey: async ({}, use, testInfo) => {
const key = [
process.env.CI_PIPELINE_ID ?? 'local',
testInfo.workerIndex,
testInfo.retry,
testInfo.testId.slice(0, 8),
].join('-');
await use(`reg-${key}`);
},
});
Worker Index بهتنهایی برای یکتایی جهانی کافی نیست؛ Run ID و Test ID را هم اضافه کنید. برای طراحی چرخهٔ ایجاد، Mask، Refresh و حذف داده به راهنمای Test Data Management مراجعه کنید.
Environment نیز باید ظرفیت موازی را تحمل کند
اگر App دارای ۲۰ Connection دیتابیس یا PSP Sandbox دارای Rate limit برابر پنج درخواست بر ثانیه است، ۳۲ Worker الزاماً سریعتر نیست. گاهی فقط Timeout و Flake تولید میکند. مالکیت، رزرو، Drift و Capacity محیط در راهنمای مدیریت محیط تست با جزئیات پوشش داده شده است.
Test Granularity را برای Scheduling اصلاح کنید
Scheduler فقط واحدی را میتواند پخش کند که Runner میشناسد. اگر ۴۰ سناریو در یک فایل سریال بستهبندی شدهاند، داشتن ۴۰ Worker کمکی نمیکند.
فایل غولآسا را بر اساس Boundary پایدار بشکنید
شکستن باید با Cohesion و هزینهٔ Setup متوازن باشد. برای مثال Checkout را میتوان به «قیمت و تخفیف»، «ایجاد سفارش»، «پرداخت و Callback» و «لغو/بازپرداخت» تقسیم کرد. اما خردکردن هر Assertion به یک Job جدا، Setup و Artifact overhead را چند برابر میکند.
Dependency صریح را از ترتیب تصادفی جدا کنید
اگر Test B واقعاً خروجی Test A را نیاز دارد، یا آنها یک Scenario واحدند یا باید Fixture مشترک نسخهدار بسازید. اتکا به ترتیب نام فایل، Retry یا Worker allocation یک قرارداد معتبر نیست.
Setup را Scope کنید
Setup ارزان و ایزوله میتواند برای هر Test اجرا شود؛ Setup پرهزینه اما Immutable شاید در سطح Worker یا Job ساخته شود. Shared mutable state، حتی اگر سریع باشد، هزینهٔ Debug و Flake را بالا میبرد. مستندات رسمی Fixtureهای Playwright تفاوت Fixtureهای Test-scoped و Worker-scoped را نشان میدهد.
Sharding با Parallelism یکی نیست
Parallelism اجرای همزمان در یک Process/ماشین یا مجموعه Workerهاست. Sharding تقسیم Corpus بین چند Job یا ماشین مستقل است. ممکن است داخل هر Shard نیز چند Worker فعال باشد. برای تصمیم درست، دو سطح ظرفیت را جدا ثبت کنید:
total concurrency = shard jobs × workers per shard
effective concurrency ≤ min(total concurrency,
independent test units,
environment capacity,
constrained resource capacity)
تقسیم مساوی بر اساس تعداد، Load Balancing نیست
شش فایل ۳۰ دقیقهای و شش فایل پنج دقیقهای هر دو «شش تست» هستند، اما Load یکسان ندارند. اگر ترتیب فایلها با Domain یا نامگذاری همبسته باشد، تقسیم پیوستهٔ لیست بدترین Long Tail را میسازد.
زمان تاریخی را وزن قرار دهید
یک راه ساده، الگوریتم Longest Processing Time یا LPT است: فایلها را نزولی بر اساس Duration تخمینی مرتب و هر فایل را به کمبارترین Shard فعلی اضافه کنید. LPT همیشه Optimal نیست، اما Baseline شفاف و ارزان خوبی است. برای تیمهای بزرگتر باید قید Browser، Fixture، Region، Resource class و Affinity نیز وارد Scheduler شود.
History آلوده را کنترل کنید
- از Median یا Winsorized estimate استفاده کنید تا یک Outlier وزن را برای هفتهها خراب نکند.
- Duration هر Browser/Project/Runner class را جدا نگه دارید.
- برای Test جدید وزن پیشفرض محافظهکارانه تعیین کنید.
- Attempt اول را از Retry جدا کنید؛ Flake نباید وزن عادی Test را مبهم کند.
- پس از تغییر معنیدار Test یا Fixture، History version را Reset یا Decay کنید.
مستندات رسمی Sharding در Playwright اجرای Shard روی ماشینهای مختلف و ادغام Blob Reportها را نشان میدهد. GitLab نیز با کلید parallel یک Job بزرگ را به Jobهای همزمان تقسیم میکند؛ جزئیات در راهنمای کنترل Jobهای GitLab CI آمده است.
آزمایش قطعی: Count-based در برابر Duration-balanced
این آزمایش با ۲۴ فایل تست ساختگی انجام شده است. Durationها دقیقه و از ۳ تا ۳۱ هستند؛ مجموع Execution برابر ۳۰۶ دقیقه است. برای هر Shard هشت دقیقه Setup و برای Merge چهار دقیقه ثابت در نظر گرفتهایم. هیچ Retry، Queue، Cache miss یا Contention وجود ندارد تا فقط اثر توزیع دیده شود.
داده و الگوریتم قابلبازتولید
const tests = [
['checkout-card',31], ['checkout-wallet',29], ['payment-callback',27],
['refund',24], ['cart-price',19], ['coupon',17], ['inventory-hold',16],
['shipping-rate',15], ['order-create',14], ['invoice',13],
['sms-notify',12], ['email-notify',11], ['profile',10], ['address',9],
['search',8], ['catalog',8], ['login',7], ['signup',7], ['wishlist',6],
['review',6], ['support',5], ['history',5], ['logout',4], ['health',3]
];
function lpt(items, shardCount) {
const bins = Array.from({ length: shardCount }, () => []);
const loads = Array(shardCount).fill(0);
for (const test of [...items].sort((a, b) => b[1] - a[1])) {
const target = loads.indexOf(Math.min(...loads));
bins[target].push(test);
loads[target] += test[1];
}
return { bins, loads };
}
const setupPerShard = 8;
const merge = 4;
const { bins, loads } = lpt(tests, 4);
const wall = setupPerShard + Math.max(...loads) + merge;
console.log({ loads, wall, bins });
نتیجهٔ تقسیم چهارتایی
| روش | بار چهار Shard | Slowest execution | Wall با Setup/Merge | عدمتوازن Slowest نسبت به Mean |
|---|---|---|---|---|
| شش فایل پیوسته برای هر Shard | ۱۴۷، ۸۱، ۴۹، ۲۹ | ۱۴۷ دقیقه | ۱۵۹ دقیقه | ۹۲٫۲٪ |
| LPT بر پایهٔ Duration | ۷۶، ۷۷، ۷۷، ۷۶ | ۷۷ دقیقه | ۸۹ دقیقه | ۰٫۷٪ |
هر دو روش دقیقاً همان ۲۴ فایل را اجرا و ۳۴۲ Runner-minute مصرف میکنند: ۳۰۶ دقیقه Execution، چهار Setup هشتدقیقهای و چهار دقیقه Merge. تفاوت ۷۰ دقیقهای Wall-clock فقط از توزیع آمده است. بنابراین اولین سؤال پیش از خرید Runner باید این باشد: «کار موجود چگونه پخش شده است؟»
منحنی ظرفیت و هزینه
| Shard | بیشترین Load با LPT | Wall-clock | Runner-minute | Speedup نسبت به ۱ | Parallel efficiency |
|---|---|---|---|---|---|
| ۱ | ۳۰۶ | ۳۱۸ | ۳۱۸ | ۱٫۰۰× | ۱۰۰٪ |
| ۲ | ۱۵۳ | ۱۶۵ | ۳۲۶ | ۱٫۹۳× | ۹۶٫۴٪ |
| ۳ | ۱۰۳ | ۱۱۵ | ۳۳۴ | ۲٫۷۷× | ۹۲٫۲٪ |
| ۴ | ۷۷ | ۸۹ | ۳۴۲ | ۳٫۵۷× | ۸۹٫۳٪ |
| ۶ | ۵۳ | ۶۵ | ۳۵۸ | ۴٫۸۹× | ۸۱٫۵٪ |
| ۸ | ۴۱ | ۵۳ | ۳۷۴ | ۶٫۰۰× | ۷۵٪ |
با هشت Shard، زمان از ۸۹ به ۵۳ دقیقه میرسد اما ۳۲ Runner-minute بیشتر مصرف میشود و Efficiency پایین میآید. این جدول هنوز خوشبینانه است؛ در دنیای واقعی ممکن است پس از چهار Shard، DB یا Sandbox اشباع و Wall-clock بدتر شود.
فرمولها
speedup(N) = T1 / TN
parallel_efficiency(N) = speedup(N) / N
runner_minutes = Σ(job execution + job setup) + merge cost
shard_imbalance = (max_shard_load - mean_shard_load) / mean_shard_load
محدودیت آزمایش: دادهها ساختگی و Deterministic هستند؛ LPT فقط یک Heuristic است؛ Duration به Runner/Browser/Environment وابسته است؛ و مدل، Queue و منبع مشترک را شبیهسازی نمیکند. از اعداد آن بهعنوان Benchmark صنعت یا وعدهٔ کاهش زمان استفاده نکنید. ارزش مثال در آشکارکردن «تعداد مساوی ≠ زمان مساوی» و ساخت Capacity Experiment است.
Capacity Experiment را چگونه اجرا کنیم؟
تعداد Worker را Configuration دائمی فرض نکنید؛ آن را متغیر آزمایش کنید. برای یک Corpus، Build، Runner image و Environment نسبتاً ثابت، چند سطح ظرفیت را تکرار کنید.
قرارداد آزمایش
Hypothesis:
افزایش total concurrency از 4 به 6، P95 زمان تصمیم را حداقل 20٪ کم میکند.
Hold constant:
suite contract، build، browser، runner class، environment، retry=0
Outcome:
P50/P95 trusted-decision time، first-actionable-failure time
Cost:
runner-minute، artifact GB، sandbox calls
Guardrails:
missing result = 0
infra-failure افزایش معنادار نداشته باشد
flake rate و 429/timeout بدتر نشود
Decision:
Adopt / Adapt / Continue / Stop
نقطهٔ Saturation را پیدا کنید
اگر از ۶ به ۸ Worker فقط سه دقیقه کم میشود اما Runner-minute، Timeout و ۴۲۹ رشد میکند، ظرفیت مؤثر به سقف رسیده است. علت را در CPU، Memory، DB connection، Network، Container scheduling یا Remote API پیدا کنید. «Worker بیشتر» خود یک درمان نیست.
بهینهسازی Setup بدون ساخت Cache خطرناک
Dependency download، Browser install، Build و Dataset provisioning گاهی بیش از خود Test زمان میگیرند. Cache میتواند مؤثر باشد، اما فقط وقتی Key، Scope، Trust و Invalidation روشن است.
چه چیزهایی معمولاً Cache میشوند؟
- پکیجهای Dependency با Key وابسته به Lockfile، OS و Runtime version؛
- Build output قابل بازتولید با Key وابسته به Source/Compiler/Config؛
- Browser binary با Version دقیق؛
- Snapshot پایهٔ Immutable برای داده، با Migration/schema version؛
- Artifactهای میانیای که Integrity آنها قابل سنجش است.
چه چیزهایی نباید کورکورانه Cache شوند؟
نتیجهٔ Pass، Session احراز هویت با عمر نامعلوم، دادهٔ mutable سفارش، Token/Secret، پاسخ شخصیسازیشده یا Snapshot بدون Schema hash. Cache باید فقط کار قابل بازتولید را حذف کند، نه Evidence جدید را.
راهنمای رسمی Dependency caching در GitHub Actions تفاوت Cache با Artifact را روشن میکند: Cache برای فایلهای قابل استفادهٔ مجدد و Artifact برای خروجی Run است. Restoreشده را ورودی غیرقابلاعتماد فرض کنید و Secret را داخل Cache نگذارید.
Hit Rate بهتنهایی موفقیت نیست
همزمان Cache hit rate، زمان Restore/Save، bytes، زمان Saved و Failure ناشی از Stale/Poisoned cache را ثبت کنید. Cache صددرصد Hit که Restore آن از Download تازه کندتر است، ارزش ندارد.
Retry و Flake Tax را پنهان نکنید
اگر Failure اول و Pass دوم بهصورت Pass ساده ثبت شود، Dashboard سریع و سبز به نظر میرسد اما هزینه و عدمقطعیت پنهان میشود.
Flake Tax را دو شکل گزارش کنید
retry_compute_tax = Σ duration of retry attempts
retry_wall_tax = trusted decision time with retries - time of initial corpus completion
initial_outcome ∈ {passed, product_failed, infra_failed, timed_out}
final_outcome ∈ {passed, failed, inconclusive}
flaky_observed = initial_failed AND later_passed_without_relevant_change
Retry ممکن است برای جمعآوری Evidence یا کاهش اختلال موقت مجاز باشد، اما نتیجهٔ اولیه، شمارهٔ Attempt و Failure signature باید باقی بماند. Fail→Pass یک Test سالم نیست.
Quarantine بودجه و تاریخ انقضا میخواهد
هر مورد Quarantine باید Owner، علت فرضی، Risk coverage از دسترفته، جایگزین موقت، تاریخ تصمیم و حداکثر عمر داشته باشد. Queue بینهایت Quarantine، Coverage را بیصدا فرسوده میکند.
ترتیب اجرا را برای Feedback مهندسی کنید
حتی با زمان پایان ثابت، میتوان Failure مفید را زودتر یافت. تستهای سریع و پرسیگنال، Smokeهای Contract، و تستهای مرتبط با تغییر میتوانند زودتر شروع شوند. اما این کار سه دام دارد.
دام اول: Starvation
تست کند یا کماحتمال نباید هرگز اجرا نشود. یک Full fallback زمانبندیشده و Aging guardrail لازم است. انتخاب تست بر اساس تغییر و ریسک را با قرارداد مقالهٔ RBT انجام دهید، نه با حذف دستی Suite.
دام دوم: Fail-fast ناقص
Fail-fast برای Feedback PR مفید است، اما اگر تمام Shardها را Cancel کند ممکن است چند Failure مستقل یا Evidence لازم برای Release از دست برود. سیاست میتواند «Fast lane fail-fast؛ Full regression جمعآوری کامل» باشد.
دام سوم: تاریخچهٔ مغرضانه
اگر فقط تستهایی که قبلاً Fail شدهاند زود اجرا شوند، نواحی جدید یا کماجرا دیده نمیشوند. Priority باید ترکیبی از Risk، Change impact، Signal، Duration، Aging و Coverage contract باشد؛ نه Failure count خام.
پژوهش و راهنمای Continuous Integration در DORA بر تستهای سریع برای Feedback زودهنگام تأکید دارد، در کنار نیاز به تستهای End-to-End کامل. سریع و جامع، دو Lane یا افق زمانی متفاوتاند؛ یکی نباید دیگری را حذف کند.
Report Merge بخشی از کیفیت است، نه کار تزئینی
وقتی تستها Shard میشوند، هر Job فقط بخشی از حقیقت را میبیند. Merge باید قبل از Gate یک Integrity Check انجام دهد.
قرارداد Evidence Merge
expected_test_ids == observed_test_ids
expected_shards == completed_or_explicitly_failed_shards
each_result has:
test_id, project, attempt, status, duration,
commit, build_id, environment, shard_id
no duplicate final identity without a declared attempt relationship
all missing/extra results => INCONCLUSIVE, never PASS
Artifactها را حتی در Failure نگه دارید
Trace، Screenshot، JUnit/Blob و Log Sanitized باید با Retention و دسترسی متناسب ذخیره شوند. دادهٔ بانکی، شماره موبایل، Token، Cookie و اطلاعات شخصی پیش از Upload ماسک شوند. Artifact ناقص میتواند Failure را غیرقابل اقدام کند؛ Artifact بیشازحد نیز هزینه و ریسک حریم خصوصی دارد.
نمونهٔ Shard و Merge
playwright-tests:
parallel:
matrix:
- SHARD: ["1/4", "2/4", "3/4", "4/4"]
script:
- npx playwright test --shard=$SHARD --reporter=blob
artifacts:
when: always
paths:
- blob-report/
merge-regression-evidence:
needs: [playwright-tests]
script:
- npx playwright merge-reports --reporter=html ./all-blob-reports
- node scripts/verify-regression-manifest.mjs
artifacts:
when: always
paths:
- playwright-report/
Syntax دقیق Artifact collection به CI شما وابسته است؛ نمونه برای نمایش قرارداد است. اگر Job شکستخورده Artifact را Upload نکند یا Merge فقط Jobهای موفق را بخواند، گزارش سبز کاذب میشود.
نمونهٔ ایرانی: Checkout، PSP و Ledger
یک Marketplace ایرانی را در نظر بگیرید که Regression آن Checkout، تخفیف، موجودی، PSP callback، Ledger، پیامک و بازپرداخت را پوشش میدهد. تیم با ۱۶ Worker اجرا را موازی میکند، اما زمان بدتر و Flake بیشتر میشود.
تشخیص
- همهٔ تستها از یک پذیرنده و Callback URL مشترک استفاده میکنند؛ Sandbox بیش از چهار جریان همزمان را Throttle میکند.
- شماره سفارش از Timestamp ثانیهای ساخته میشود و Workerها Collision دارند.
- تستهای Ledger منتظر Job reconciliation مشترک میمانند.
- یک فایل ۴۸ دقیقهای تمام سناریوهای بازپرداخت را سریال اجرا میکند.
- Retry دو بار فعال است و Pass نهایی، Failure اول را در Dashboard پنهان میکند.
- Shard ناموفق Blob report ندارد؛ Merge فقط نتایج حاضر را Pass میکند.
اصلاح مرحلهای
- Concurrency مسیر PSP روی چهار نگه داشته و بقیهٔ تستهای API/Domain روی Pool جدا اجرا میشوند.
- Order ID از Run ID + Worker + Test ID و Idempotency Key مستقل ساخته میشود.
- رکورد مبلغ به IRR بهصورت Canonical ذخیره و نمایش تومان جدا Assert میشود؛ ارقام فارسی، عربی و لاتین Test data صریح دارند.
- Callbackهای duplicate، late و timeout-after-commit با Stub قرارداددار و چند Integration محدود Sandbox سنجیده میشوند.
- فایل Refund بر اساس State transitionهای coherent شکسته و با زمان تاریخی توزیع میشود.
- Attempt اول حفظ، Retry tax جدا و Quarantine دارای Owner/Expiry میشود.
- Merge، تعداد Test ID و Shard مورد انتظار را کنترل و در Missing result وضعیت INCONCLUSIVE صادر میکند.
Oracle مستقل
سبزشدن UI کافی نیست. نتیجهٔ پرداخت باید با PSP response/callback، Order state، Ledger entry، Outbox/notification و Reconciliation خوانده شود. برای Cutoffهای مالی، UTC و Asia/Tehran هر دو ثبت شوند. تغییر تقویم یا رشتهٔ «تومان» نباید باعث مقایسهٔ اشتباه ریال/تومان شود.
Test Pyramid را به Checklist عددی تبدیل نکنید
جابجایی Coverage از UI کند به Contract/Integration یا Unit میتواند زمان را کم و تشخیص را بهتر کند؛ اما نسبت جهانی ۷۰/۲۰/۱۰ وجود ندارد. هر Behavior باید در ارزانترین سطحی سنجیده شود که Failure mode و Confidence مورد نیاز را واقعاً مشاهده میکند.
چارچوب SMURF در Google Testing Blog پیشنهاد میکند Trade-offها را با Speed، Maintainability، Utilization، Reliability و Fidelity بسنجیم. یک E2E کند اما ضروری را نباید صرفاً برای زیباترشدن نسبتها حذف کرد؛ شاید بتوان Setup، Boundary یا Oracle آن را بازطراحی کرد.
ترتیب بهینهسازی: از کمریسک تا معماری
مرحلهٔ ۱: اتلاف قابل مشاهده را حذف کنید
- Sleep ثابت را با انتظار Condition-based و Deadline محدود جایگزین کنید.
- دانلود Dependency و Build تکراری را با Cache versioned بهبود دهید.
- Artifact غیرضروری را کم و Upload را فشرده کنید.
- Testهای Disabled، Duplicate و بدون Oracle را با تصمیم مالک بازبینی کنید؛ حذف Coverage نیازمند ثبت Risk است.
مرحلهٔ ۲: Long Tail را مهندسی کنید
- Top ۱۰ تست/فایل کند را بر اساس سهم از Critical Path مرتب کنید.
- فایلهای بزرگ را با حفظ Cohesion بشکنید.
- Remote callهای تکراری، Polling و Dataset بیشازحد را پیدا کنید.
- Duration variance بالا را مانند Flake زمانی بررسی کنید، نه فقط Median کند.
مرحلهٔ ۳: Isolation و Resource Contract
- Namespace داده، Pool منابع و Lockهای مجاز را صریح کنید.
- Fixtureهای Worker-scoped را Immutable یا با Partition مطمئن بسازید.
- برای PSP، پیامک، ایمیل و سرویسهای بیرونی Rate budget تعریف کنید.
مرحلهٔ ۴: Scheduling و Capacity
- Count-based را با Duration-aware baseline مقایسه کنید.
- منحنی ۱/۲/۴/۶/۸ Worker را با Cost و Guardrail بسازید.
- Shard imbalance و idle tail را Alert کنید.
مرحلهٔ ۵: معماری Coverage
رفتارهای مناسب را از UI به API/Contract/Component منتقل کنید، بدون حذف Journeyهای حیاتی. این تغییر باید با Defect detection، Fidelity و Maintainability ارزیابی شود. مبانی انتخاب ابزار و کاندید اتوماسیون در راهنمای اتوماسیون تست آمده است.
Dashboard تصمیم، نه مسابقهٔ سرعت
یک Dashboard خوب با سؤال شروع میشود. «چرا Feedback دیر شد؟»، «کجا هزینه بدون کاهش زمان رشد کرد؟» و «آیا Evidence هنوز کامل و قابل اعتماد است؟»
Outcomeها
- P50/P95 زمان تا اولین Failure قابل اقدام؛
- P50/P95 زمان تا تصمیم قابلاعتماد؛
- درصد PRهایی که Feedback در SLO توافقی گرفتهاند؛
- زمان Developer wait فقط با تعریف و دادهٔ معتبر، نه حدس.
Driverها
- Queue، Provision، Setup، Execution، Retry، Upload و Merge؛
- Critical Path و بیشترین Load Shard؛
- Shard imbalance و Worker utilization؛
- Cache hit/miss همراه با زمان واقعاً ذخیرهشده؛
- Top Long-tail tests و Duration variance.
Guardrailها
- Expected در برابر Observed test result و Shard؛
- Flake/Retry/Infra-failure rate با denominator ثابت؛
- Quarantine age و Risk coverage معلق؛
- Escaped regression با Cohort و Severity، بدون نسبتدادن خودکار علت به QA؛
- Runner-minute، Storage و External-call cost؛
- ۴۲۹، DB saturation، Timeout و Resource lock wait.
تعریف Metric را Freeze کنید
برای هر Metric، Name، Question، Numerator، Denominator، Clock start/end، Exclusion، Dimensions، Owner و Version را ثبت کنید. اگر همزمان Corpus کوچک، Runner قویتر و Retry بیشتر شده، کاهش Runtime را به یک تغییر نسبت ندهید.
برنامهٔ ۳۰روزه برای کاهش زمان تست رگرسیون
روزهای ۱ تا ۵: Instrument و Freeze
- قرارداد Suite، Build، Environment و Retry را نسخهدار کنید.
- Timeline فازها و Test-level duration/attempt را جمع کنید.
- Missing result و Report integrity را به Gate تبدیل کنید.
- Baseline را به تفکیک PR/Nightly و Runner class بسازید.
روزهای ۶ تا ۱۰: Critical Path و Flake Tax
- Top Long-tail و منابع مشترک را استخراج کنید.
- Retry compute/wall tax را جدا گزارش دهید.
- سه Dependency پنهان یا Shared-state پراثر را بازتولید کنید.
- یک Target Outcome و دو Guardrail انتخاب کنید.
روزهای ۱۱ تا ۱۵: Isolation و Granularity
- Namespace را برای User/Order/Payment per Worker اعمال کنید.
- یک فایل غولآسا را با Boundary منطقی بشکنید.
- Fixture scope و Cleanup را Stress-test کنید.
- Quarantineها را Owner، Risk و Expiry بدهید.
روزهای ۱۶ تا ۲۰: Load Balancing Pilot
- History پاک و Versioned بسازید.
- Count-based و LPT را در Shadow run با Corpus یکسان مقایسه کنید.
- Shard imbalance، Wall-clock و Runner-minute را ثبت کنید.
- در Missing evidence یا افزایش Flake، Pilot را Hold کنید.
روزهای ۲۱ تا ۲۵: Capacity و Cache
- منحنی Worker/Shards را تا Saturation اجرا کنید.
- یک Cache پرهزینه را با Key/Invalidation/Trust contract اصلاح کنید.
- DB/PSP/Network capacity را کنار Test concurrency رسم کنید.
روزهای ۲۶ تا ۳۰: تصمیم و Institutionalize
- Before/After را با Cohort، Sample و Confounder منتشر کنید.
- Adopt/Adapt/Continue/Stop را ثبت کنید.
- SLO زمان تصمیم و Cost budget را با مالک تعیین کنید.
- هفتگی Long Tail/Flake و ماهانه Capacity/Architecture را بازبینی کنید.
نمونهٔ Before/After معتبر چگونه گزارش میشود؟
گزارش زیر فقط قالب است و اعداد آن باید از سامانهٔ خودتان بیاید:
Scope: PR full-regression, Chrome, runner-class c8, suite-contract v17
Window: 20 runs before / 20 runs after
Change: duration-aware 4-shard scheduling + per-worker order namespace
Before:
trusted-decision P50/P95 = 71 / 104 min
shard imbalance P50 = 48%
retry wall tax P50 = 11 min
missing-result runs = 1/20
After:
trusted-decision P50/P95 = 46 / 63 min
shard imbalance P50 = 7%
retry wall tax P50 = 5 min
missing-result runs = 0/20
Cost:
runner-minute P50 = 338 → 344
Limits:
build volume was lower in week two;
no causal claim about escaped defects;
recheck after 60 runs.
«زمان ۳۵٪ کم شد» بدون Corpus، Sample، Percentile، Cost و Guardrail یک ادعای بازاریابی است، نه Evidence بهبود.
Anti-patternهای رایج
- Worker بیشتر بدون Baseline: هزینه و Contention رشد میکند و علت اصلی نامعلوم میماند.
- Shard مساوی با Test count: Long Tail از Duration نابرابر ساخته میشود.
- Retry تا سبزشدن: Flake و Product failure احتمالی زیر Pass نهایی پنهان میشود.
- Fail-fast برای همهچیز: Evidence مستقل بعدی از دست میرود.
- Cache بدون Version key: Build یا دادهٔ Stale نتیجهٔ غیرقابل اعتماد میسازد.
- Shared account/data: اجرای موازی Collision و ترتیب پنهان ایجاد میکند.
- یک Concurrency برای همهٔ تستها: ظرفیت PSP و DB با تست Unit یکسان فرض میشود.
- نادیدهگرفتن Queue: تیم کد تست را بهینه میکند در حالی که Runner pool گلوگاه است.
- حذف تست کند بدون Risk review: Runtime کم اما Coverage contract شکسته میشود.
- Metric میانگین تنها: P95 و Runهای بحرانی پنهان میمانند.
- Merge فقط Jobهای موفق: Missing shard به Pass کاذب تبدیل میشود.
- ذخیرهٔ Secret/PII در Cache و Artifact: بهینهسازی سرعت به رخداد امنیتی بدل میشود.
چکلیست آمادگی Regression Suite سریع و قابل اعتماد
- آیا Queue، Setup، Execution، Retry و Merge جدا اندازهگیری میشوند؟
- آیا P50/P95 و Distribution، نه فقط Average، دیده میشود؟
- آیا Critical Path و بزرگترین Test unit معلوم است؟
- آیا هر تست Test ID پایدار و Duration history نسخهدار دارد؟
- آیا داده به Run/Worker/Test namespace میشود؟
- آیا منابع محدود مانند PSP/DB دارای Concurrency budget هستند؟
- آیا Shard بر اساس Duration و Constraint متوازن میشود؟
- آیا افزایش Worker با منحنی Wall-clock/Cost سنجیده شده است؟
- آیا Attempt اول و Retry هر دو باقی میمانند؟
- آیا Quarantine مالک، Risk، جایگزین و Expiry دارد؟
- آیا Cache key شامل Dependency/OS/Runtime/Schema مناسب است؟
- آیا Cache بدون آن نیز قابل بازتولید است؟
- آیا Artifactهای Failure همیشه و بهشکل Sanitized نگه داشته میشوند؟
- آیا Expected/Observed Test IDs پس از Merge برابرند؟
- آیا Missing shard نتیجه را INCONCLUSIVE میکند؟
- آیا Fast feedback از Full evidence contract جدا اما متصل است؟
- آیا حذف یا انتقال تست با Risk/Fidelity review انجام میشود؟
- آیا Metric definition، Cohort و Exclusion نسخهدار است؟
- آیا Before/After همراه Cost و Guardrail گزارش میشود؟
- آیا تصمیم Adopt/Adapt/Continue/Stop و تاریخ بازبینی ثبت میشود؟
پرسشهای متداول درباره بهینهسازی تست رگرسیون
۱. بهترین تعداد Worker برای تست رگرسیون چند است؟
عدد جهانی وجود ندارد. بهترین نقطه جایی است که SLO زمان تصمیم را با Runner-minute قابل قبول و بدون افزایش Flake، Timeout، ۴۲۹ یا Missing evidence برآورده کند. آن را با Capacity Experiment و چند سطح Worker پیدا کنید.
۲. Sharding چه تفاوتی با اجرای موازی دارد؟
Sharding، Corpus را میان Job یا ماشینهای مستقل تقسیم میکند؛ Parallelism اجرای همزمان داخل یک Job/ماشین یا بین Workerهاست. ممکن است هر Shard نیز چند Worker داشته باشد. حاصلضرب این دو فقط Concurrency اسمی است و ظرفیت محیط، استقلال تست و Critical Path مقدار مؤثر را محدود میکنند.
۳. آیا تستهای کند را حذف کنیم؟
نه بر اساس Duration تنها. ابتدا Risk و Failure mode، سطح مناسب تست، Oracle، Duplication و قابلیت انتقال به Unit/API/Contract را بررسی کنید. حذف یا کاهش Frequency باید Owner، Evidence جایگزین و Safe fallback داشته باشد.
۴. آیا Retry راه مناسبی برای سریعترشدن Pipeline است؟
Retry گاهی برای تشخیص و تحمل اختلال موقت کاربرد دارد، اما Runtime را بیشتر و Signal را مبهم میکند. Failure اول، همهٔ Attemptها، Retry tax و Flaky classification را نگه دارید؛ Pass بعد از Retry را Pass سالم گزارش نکنید.
۵. مهمترین متریک Regression Test Optimization چیست؟
برای بیشتر تیمها «P95 زمان تا تصمیم قابلاعتماد» Outcome خوبی است، به شرطی که Runner cost، Flake/Infra failure، Missing result و Risk coverage Guardrail آن باشند. Runtime متوسط بهتنهایی رفتار Long Tail و اعتمادپذیری را نشان نمیدهد.
جمعبندی
سریعکردن Regression Suite یک مسئلهٔ صرفاً ابزاری نیست؛ ترکیبی از Measurement، Scheduling، Data/Environment isolation، Capacity، Reliability و Evidence integrity است. Baseline را به فازها بشکنید، Critical Path را پیدا کنید، Granularity و استقلال را اصلاح کنید، Shardها را با Duration و قید منابع متوازن سازید و سپس Worker را تا نقطهٔ اقتصادی افزایش دهید.
معیار موفقیت، «تعداد تست در دقیقه» نیست. موفقیت یعنی تیم زودتر به تصمیمی برسد که Corpus، Attempt، Build، Environment و شواهدش کامل و قابل بازتولید است—و بتواند نشان دهد این کاهش زمان با چه هزینه و بدون نقض کدام Guardrail به دست آمده است.

