بیشتر Suiteهای Cucumber از روز اول خراب طراحی نمیشوند؛ بهتدریج فرسوده میشوند. هر Feature یک فایل Step مخصوص میگیرد، CommonSteps به انبار همهچیز تبدیل میشود، Hookها پیششرطهای پنهان میسازند، State سراسری اجرای موازی را میشکند و Tagها آنقدر زیاد میشوند که هیچکس نمیداند Pipeline دقیقاً چه چیزی را اجرا کرده است.
راهحل، نام پوشه زیباتر یا DRY افراطی نیست. یک معماری سالم Cucumber باید سه قرارداد را همزمان حفظ کند: Feature برای انسان و زبان دامنه، Glue نازک و یکتا برای اتصال، و Driver/Adapter قابل تست برای اجرای رفتار. این راهنما ساختار پروژه، Step Definition، Cucumber Expressions، Scenario State، Hook، Tag، داده تست، Parallel Execution، CI و گزارش را با تمرکز بر Cucumber-JVM توضیح میدهد و تفاوت پیادهسازیها را صریح نگه میدارد.
خلاصه اجرایی: معماری پایدار Cucumber در یک نگاه
- Featureها را بر اساس Bounded Context و توانمندی کسبوکار بچینید، نه صفحه، Ticket یا Microservice؛
- Step Definitionها را بر اساس مفهوم دامنه سازمان دهید، نه یک فایل Step برای هر Feature؛
- Glue فقط تبدیل پارامتر، فراخوانی Driver و Assertion را انجام دهد؛
- State فقط داخل همان Scenario و بدون Static/Singleton مشترک بماند؛
- Background زمینه قابل مشاهده کسبوکار و Hook چرخه عمر فنی را نگه دارد؛
- Tagها Taxonomy کوچک با مالک و کاربرد اجرایی روشن داشته باشند؛
- Execution Profile را از معنای Feature جدا کنید؛
- Parallelism را پس از اثبات Isolation و با سقف ظرفیت وابستگی فعال کنید؛
- بیشتر رفتارها را از Port/API/Service و فقط مسیرهای لازم را از UI اجرا کنید؛
- گزارش باید Commit، Build، Environment، Filter، Retry و شاهد خطا را نشان دهد.
مرز این راهنما: Formulation در برابر معماری اجرا
Cucumber ابزار پشتیبان BDD است، نه خود BDD. Discovery، Rule و مثال باید پیش از Glue شکل گرفته باشند. راهنمای نوشتن سناریوی Gherkin کیفیت Given/When/Then، Example Mapping، گرکین فارسی و انتخاب مثال را پوشش میدهد. ستون BDD با Cucumber نیز مسیر کامل از Discovery تا Automation را توضیح میدهد. مقاله حاضر از لحظهای شروع میشود که Example معتبر داریم و باید یک Suite اجرایی قابل نگهداری بسازیم.
مستندات رسمی Cucumber میگوید Featureهای Gherkin به Step Definition متصل میشوند و Runner رفتار نرمافزار را با Specification بررسی میکند. اما CLI، JUnit Platform، Annotationها، Hookها، World و DI میان Cucumber-JVM، Cucumber-JS و Ruby یکسان نیستند. هرجا مثال این مقاله Java/JVM است، آن را قانون همه پیادهسازیها تلقی نکنید و API نسخه نصبشده را با مستندات همان Runtime تطبیق دهید.
مدل معماری: Domain Specification تا System Driver
یک مسیر سالم اجرا چنین است:
- Feature/Rule/Scenario: قرارداد خوانا با واژگان کسبوکار؛
- Expression و Type: تطبیق یکتای Step و تبدیل متن به نوع دامنه؛
- Step Definition: آداپتور بسیار نازک؛
- Scenario Context: State محدود به یک مثال؛
- Application Driver: عملیات و مشاهده سطحبالای دامنه؛
- Port Adapter: API، UI، Message، Database Fixture یا Stub؛
- Assertion/Evidence: مقایسه Actual/Expected و شاهد تشخیصی.
Feature نباید بداند «چگونه» و Step Definition نباید منطق محاسبه محصول را دوباره بسازد. Driver به زبان Automation میگوید «پیشفاکتور بگیر»؛ Adapter تصمیم میگیرد این کار با Service API، HTTP یا مرورگر انجام شود. این مرزبندی امکان میدهد همان Rule با کمترین هزینه در سطح مناسب اجرا شود.
ساختار پیشنهادی Repository
src/test/
├── resources/
│ ├── features/
│ │ ├── billing/
│ │ │ ├── discounts.feature
│ │ │ └── refunds.feature
│ │ └── identity/
│ │ └── account-locking.feature
│ └── cucumber.properties
└── java/ir/example/acceptance/
├── steps/
│ ├── BillingSteps.java
│ └── IdentitySteps.java
├── types/
│ └── DomainParameterTypes.java
├── context/
│ └── ScenarioContext.java
├── drivers/
│ ├── BillingDriver.java
│ └── IdentityDriver.java
├── adapters/
│ ├── api/
│ ├── ui/
│ └── stub/
├── fixtures/
├── assertions/
└── hooks/
└── TechnicalLifecycleHooks.java
این درخت نسخه اجباری نیست. اصل پایدار، جهت وابستگی است: Step به Driver وابسته میشود، نه Driver به Cucumber؛ Adapter جزئیات ابزار را نگه میدارد؛ Context Scenario-scoped است؛ و Feature/Step با مفاهیم Billing و Identity نامگذاری میشوند.
سازماندهی Feature File بر اساس دامنه
مرز دامنه، نه ساختار UI یا کد
پوشههای login_page، controllers یا نام Microservice معمولاً زبان پیادهسازیاند. Featureها را حول قابلیتهای پایدارتری مانند Refund، Credit Limit، Account Recovery و Settlement بچینید. اگر UI از وب به موبایل تغییر کرد یا یک سرویس شکسته شد، Specification دامنه نباید جابهجا شود.
یک فایل، یک Capability منسجم
Gherkin در هر فایل فقط یک Feature دارد. از Rule برای گروهکردن مثالهای هر قانون استفاده کنید. فایل عظیم «payments.feature» با دهها Rule از قبض تا بازپرداخت، Review و Ownership را دشوار میکند؛ فایل بسیار ریز برای هر Scenario نیز جستوجو و تغییر را پراکنده میسازد. Cohesion و نرخ تغییر مشترک معیار بهتری از تعداد خطاند.
مسیر فایل قرارداد Discovery نیست
مسیر features/billing/refunds.feature کمک ناوبری است؛ ثابت نمیکند Rule درست کشف شده یا صاحب دامنه آن را پذیرفته است. Ticket ID را در عنوان Feature نگذارید. اگر Traceability لازم است، آن را با Tag استاندارد یا Metadata بیرونی نگه دارید تا متن پس از بستهشدن Ticket معنا داشته باشد.
Step Definition را به Feature قفل نکنید
راهنمای رسمی ضدالگوهای Cucumber، Feature-coupled Step Definition را عامل انفجار Step، تکرار و هزینه نگهداری میداند و سازماندهی بر اساس مفهوم دامنه را توصیه میکند. بنابراین refunds_feature_steps.java را فقط چون فایلی به این نام دارید نسازید. Stepهای «سفارش»، «اعتبار» و «بازپرداخت» میتوانند در چند Feature دامنه بهکار روند.
DRY هدف نیست؛ زبان یکتا و منسجم هدف است
عمومیکردن زودهنگام Stepها به عباراتی مانند «کاربر عملیات X را روی Y انجام میدهد» معنا را از بین میبرد. از سوی دیگر، ساخت Step تازه برای هر جمله هم Glue را منفجر میکند. ابتدا واژگان دامنه را تثبیت کنید، سپس عبارتهایی را یکی کنید که واقعاً یک مفهوم و یک رفتار تبدیل دارند. شباهت متن بهتنهایی دلیل Reuse نیست.
Stepها را از داخل Step دیگر صدا نزنید
Composition باید با قابلیتهای زبان برنامهنویسی انجام شود. اگر دو Step عملی مشترک دارند، یک Driver/Helper دامنه را استخراج و هر دو آن را فراخوانی کنند. فراخوانی متن Step از Step Definition، مسیر اجرا و خطا را مبهم و Coupling را پنهان میکند؛ مستندات رسمی نیز این روش را حتی در پیادهسازیهایی که ممکن است پشتیبانی کنند توصیه نمیکند.
Cucumber Expression و نوع دامنه
مرجع Cucumber Expressions آن را جایگزینی خواناتر برای Regular Expression معرفی میکند. نوعهای داخلی مانند {int}، {float}، {word} و {string} وجود دارند و میتوان Parameter Type سفارشی ساخت. Expression و RegExp را در یک عبارت با هم مخلوط نکنید.
نوع سفارشی برای پول ایرانی
پخشکردن تبدیل ارقام فارسی، جداکننده هزارگان و ریال در چند Step منشأ اختلاف است. یک نوع دامنه {rial} بسازید که متن را Normalize، واحد را معتبر و مقدار را به Money تبدیل کند. نمونه زیر برای Cucumber-JVM و عمداً خلاصه است؛ تابع Normalizer باید Unit Test مستقل داشته باشد:
@ParameterType("[۰-۹0-9٬,]+")
public Money rial(String raw) {
String ascii = PersianNumbers.toAscii(raw)
.replace("٬", "")
.replace(",", "");
return Money.rial(new BigInteger(ascii));
}
@Given("مبلغ کالاهای سفارش {rial} است")
public void orderItemsAmountIs(Money amount) {
context.setDraftOrder(OrderDraft.withItemsAmount(amount));
}
Regex را آنقدر باز نگذارید که واحد، اعشار یا متن نامعتبر را ببلعد. تومان را یا نوع جداگانه کنید یا در خود Step صریح بنویسید؛ تبدیل ضمنی واحد در Glue یک ریسک مالی است.
Given/When/Then بخشی از کلید تطبیق نیست
در API رسمی Cucumber، نوع کلیدواژه برای ثبت و Match کردن Step Definition اهمیت ندارد. بنابراین دو تعریف با متن یکسان زیر Given و Then دو Step متمایز نمیسازند و میتوانند Ambiguous شوند. متن را از نظر دامنه متمایز کنید: «موجودی حساب ۵۰۰ هزار ریال است» در برابر «موجودی حساب ۵۰۰ هزار ریال میشود».
Undefined، Pending، Skipped و Ambiguous را سبز حساب نکنید
Cucumber API وضعیتهای Step را تفکیک میکند. اگر تعریف پیدا نشود Step، Undefined و ادامه Scenario، Skipped میشود؛ Pending یعنی کار باقی است؛ چند Match نتیجه Ambiguous میدهد. همچنین بازگرداندن false لزوماً Step را Fail نمیکند؛ Step باید Exception/Assertion failure ایجاد کند. مستندات Checking Assertions تصریح میکند Cucumber کتابخانه Assertion مستقل ارائه نمیکند و باید از ابزار مناسب Runtime استفاده شود.
Step Definition نازک و Driver دامنه ضخیمتر
یک Step خوب معمولاً فقط سه کار میکند: ورودی تبدیلشده را میگیرد، Driver را فراخوانی میکند و نتیجه لازم را در Context نگه میدارد یا Assert میکند. Retry، HTTP Client، Selector، SQL، Sleep و محاسبه Rule در Step Definition جای ندارند.
نمونه Cucumber-JVM برای پیشفاکتور
public final class BillingSteps {
private final ScenarioContext context;
private final BillingDriver billing;
public BillingSteps(ScenarioContext context, BillingDriver billing) {
this.context = context;
this.billing = billing;
}
@When("مشتری کد {word} را برای سفارش اعمال میکند")
public void applyDiscount(String code) {
Quote quote = billing.quote(context.draftOrder(), code);
context.setQuote(quote);
}
@Then("مبلغ قابل پرداخت {rial} است")
public void payableAmountIs(Money expected) {
assertThat(context.quote().payable()).isEqualTo(expected);
}
}
Driver میتواند Implementationهای ApiBillingDriver، UiBillingDriver و InProcessBillingDriver داشته باشد. Step همان واژگان را حفظ میکند و Profile تصمیم میگیرد کدام Adapter تزریق شود. Page Object فقط داخل Adapter رابط وب مفید است؛ معماری عمومی Suite را نباید به Page Object محدود کرد.
منطق مورد انتظار را در تست بازنویسی نکنید
اگر محصول تخفیف را با فرمولی محاسبه میکند و Step نیز همان فرمول را کپی کند، هر دو میتوانند یک اشتباه داشته باشند و تست سبز بماند. Expected را از مثال توافقشده بگیرید. برای محاسبات گستردهتر، Oracle مستقل، جدول تصمیم یا Propertyها را در تست سطح پایینتر نگه دارید.
Scenario State و تزریق وابستگی
اصل بنیادی: State میان Scenarioها به اشتراک گذاشته نشود. صفحه رسمی State در Cucumber میگوید Cucumber-JVM پیش از هر Scenario نمونه تازهای از کلاسهای Glue میسازد و برای اشتراک State میان Stepهای همان Scenario میتوان از DI بهره گرفت؛ اگر برنامه DI دیگری ندارد، مستندات JVM برای سازماندهی بهتر PicoContainer را پیشنهاد میکند.
ScenarioContext باید کوچک و Typed باشد
- ورودی و خروجی لازم برای Step بعدی را نگه دارد؛
- از
Map<String,Object>و Keyهای متنی پرهیز کند؛ - Service Locator یا Container همه وابستگیها نشود؛
- Entity تولید واقعی را بیدلیل در حافظه نگه ندارد؛
- پس از هر Scenario دور انداخته شود؛
- Secret یا داده شخصی را وارد Attachment نکند.
چه چیزهایی ممنوعاند؟
Static field، Singleton قابل تغییر، Session مرورگر مشترک، شناسه سفارش ثابت، فایل خروجی مشترک و Clock واقعی، Isolation را میشکنند. اجرای ترتیبی ممکن است مشکل را پنهان کند؛ Scenarioها را با ترتیب متفاوت و سپس Parallel اجرا کنید تا نشت State آشکار شود.
Background یا Hook؟ مرز مشاهدهپذیری
Background زمینهای است که برای فهم Rule لازم و برای خواننده Feature قابل مشاهده است؛ مانند «حساب فروشنده تعلیق است». Hook چرخه عمر فنی را مدیریت میکند؛ مانند ساخت Browser Driver، آغاز Trace یا پاکسازی Namespace داده تست. پنهانکردن شرط کسبوکار در Hook باعث میشود Scenario هنگام خواندن معنای کامل نداشته باشد.
Hook کم، محلی و Idempotent
Beforeفقط Resource لازم را آماده کند؛Afterحتی پس از Failure بتواند Cleanup کند؛- بستن Resource در
finallyباشد؛ - Attachment شکست کمینه، Redacted و دارای Correlation ID باشد؛
- Hook شرطی با Tag فقط وقتی Resource واقعاً متفاوت است؛
- BeforeAll/AfterAll State قابل تغییر Scenario نسازند؛
- Hook به ترتیب پنهانی چند Hook دیگر متکی نباشد.
API رسمی میگوید After پس از Scenario حتی در حالت Failed، Undefined، Pending یا Skipped اجرا میشود؛ اما ترتیب، امضای Annotation، BeforeStep/AfterStep و Global Hook در Runtimeها تفاوت دارند. نمونه JVM را به JavaScript یا Ruby کپی نکنید. وقتی ترتیب پیچیده شد، چرخه عمر را در یک Orchestrator صریح جمع کنید.
Screenshot و Report میتوانند داده حساس نشت دهند
Screenshot صفحه پرداخت، Header درخواست، Payload پیامک و Dump دیتابیس ممکن است شماره موبایل، کد ملی، Token یا اطلاعات کارت را وارد Artifact CI کنند. پیش از Attach، Masking و Allowlist فیلد داشته باشید؛ سطح دسترسی، Retention و محل ذخیره گزارش را مثل داده تست مدیریت کنید.
Taxonomy تگها: کم، قابل پیشبینی و دارای مالک
Tagها برای سازماندهی، انتخاب اجرا و Hook شرطیاند. Tag بالای Feature به Rule/Scenario/Examples ارث میرسد؛ Tag روی Background یا Step مجاز نیست. یک Registry کوچک بسازید:
- دامنه:
@billing،@identity؛ معمولاً در Feature و بهصورت ارثی؛ - ریسک/تعهد:
@risk-critical،@financial-integrity؛ با معیار ورود روشن؛ - Resource:
@browser،@external-psp؛ برای Profile/Hook فنی؛ - Traceability:
@REQ-142فقط اگر فرایند سازمان الزام میکند؛ - وضعیت موقت:
@quarantineهمراه رکورد بیرونی مالک، دلیل و تاریخ انقضا.
Tagهایی که بوی مشکل میدهند
@run_meو@dont_runبدون سیاست؛@high_priorityبدون تعریف و مالک؛@chromeروی رفتار مستقل از مرورگر؛@stagingبرای مخلوطکردن Configuration محیط با Specification؛@wipدائمی که Failure را پنهان میکند؛- تکرار نام پوشه/Feature در هر Scenario؛
- ترکیب چند محور در یک Tag مانند
@fast_billing_smoke.
Tag Expression باید نسخهپذیر باشد
Filter Pipeline را در Repository نگه دارید و در گزارش چاپ کنید. نمونه Cucumber-JVM:
mvn test -Dcucumber.filter.tags="@risk-critical and not @quarantine"
این فقط Syntax انتخاب است؛ ثابت نمیکند همه رفتارهای بحرانی درست Tag شدهاند. یک Test کوچک برای Taxonomy بنویسید: Tag ناشناخته، Scenario قرنطینه بدون Ticket/Expiry و Feature فاقد مالک دامنه باید در CI هشدار یا Fail ایجاد کند.
داده تست، Fixture و وابستگی بیرونی
داده را از API/Builder بسازید، نه از مسیر UI
اگر رفتار مورد آزمون «بازپرداخت» است، ساخت فروشنده، سفارش و پرداخت از UI زمان و Flake را بیدلیل زیاد میکند. Fixture API یا Builder سطح دامنه State لازم را سریع میسازد و Scenario فقط رویداد مورد آزمون را اجرا میکند. داده باید حداقل، معتبر و متعلق به همان Scenario باشد.
Namespace یکتا و Cleanup دوگانه
- شناسه را با Run ID و Scenario ID یکتا کنید؛
- Cleanup در After را Idempotent بسازید؛
- برای Crash قبل از After، Job جمعآوری داده منقضی داشته باشید؛
- Fixture را از داده تولید و PII جدا کنید؛
- زمان انقضا و مالک Dataset را ثبت کنید؛
- عملیات مالی را در Sandbox یا Simulator مجاز انجام دهید.
Stub برای PSP، پیامک و سرویس محدود
وابستگیهای بیرونی ایران ممکن است سهمیه، IP Allowlist، هزینه، قطعی یا محدودیت دسترسی داشته باشند. برای بیشتر Scenarioها از Stub رفتاری با Fixture نسخهپذیر استفاده و سازگاری Stub را با Contract Test مستقل بررسی کنید. صفحه رسمی Mocking و Stubbing در Cucumber نیز برای سامانه بیرونی Stub را بر Mock ترجیح میدهد. راهنمای مجازیسازی سرویس با WireMock پیادهسازی این مرز را پوشش میدهد.
معماری تستپذیر: همه Scenarioها از UI عبور نکنند
راهنمای رسمی Testable Architecture بر جداسازی منطق از زیرساخت، Ports and Adapters، تست سریع از Port و تعداد کمی تست Full-stack تأکید میکند. UI، دیتابیس، Queue و Web Service IO گران و شکنندهاند؛ Adapter اجازه میدهد بخش بزرگی از Ruleها بدون عبور از همه آنها بررسی شوند و Contract Test اطمینان مرز را بسازد.
- In-process/Domain Port: Ruleهای محاسباتی و جریان اصلی سریع؛
- Service/API: قرارداد ورودی/خروجی و Integration داخلی؛
- Stubbed External: خطا، Timeout و پاسخهای PSP/SMS؛
- Full-stack: تعداد کم برای Wiring، UI و مسیر واقعی حیاتی؛
- Production check: Smoke محدود و بدون اثر مالی واقعی.
انتخاب سطح باید از ریسک بیاید. هرم تست توزیع Suite و تستپذیری نرمافزار طراحی Port، Clock و مشاهدهپذیری را عمیقتر توضیح میدهند.
اجرای موازی فقط بعد از اثبات Isolation
صفحه رسمی Parallel Execution جزئیات JVM را برای JUnit ۵/۴، TestNG و CLI جدا میکند؛ حتی Granularity اجرا با Runner فرق دارد. بنابراین عبارت کلی «یک Runner class بسازید و Parallel کنید» قابل اتکا نیست. Configuration را با نسخه و Test Engine خود بسنجید.
آزمون آمادگی Parallel
- هیچ State قابل تغییر Static/Singleton نداریم؛
- Driver و Context Scenario-scoped هستند؛
- کاربر، سفارش، فایل، Port و Queue هر Scenario یکتاست؛
- Clock، Random Seed و Locale قابل کنترلاند؛
- Cleanup با اجرای همزمان داده دیگری را حذف نمیکند؛
- وابستگی Rate Limit و Pool کافی دارد؛
- گزارش Scenario و Thread/Worker را تفکیک میکند؛
- Scenario با اجرای تنها، ترتیبی و موازی نتیجه یکسان دارد.
تعداد Thread را از ظرفیت محاسبه کنید
useUnlimitedThreads معیار عملکرد نیست. سقف را از CPU Worker، Connection Pool، ظرفیت Environment، سهمیه PSP/پیامک و هدف Feedback تعیین کنید. با ۲ Worker شروع، نرخ خطا و p95 Runtime را ثبت و مرحلهای افزایش دهید. اگر Throughput بالا میرود اما Flake، ۴۲۹ یا Timeout رشد میکند، ظرفیت واقعی را رد کردهاید.
CI، Profile و گزارش قابل ممیزی
راهنمای رسمی Cucumber در CI بر Exit Code غیرصفر هنگام Failure و خروجی قابل فهم برای CI تکیه دارد. یک Pipeline عملی میتواند این Laneها را داشته باشد:
- Static: Parse، Formatter، Tag Policy و Forbidden Secret؛
- Glue contract: Compile، Type transformer و صفر Undefined/Ambiguous؛
- PR fast: Ruleهای سریع از Port/API بدون External؛
- Integration: Database/Queue/Stub و Contractها؛
- Critical full-stack: Tagهای محدود Browser/External؛
- Scheduled: Matrix مرورگر/Locale، Soak یا External Sandbox؛
- Publish evidence: JUnit/Messages/HTML و Attachmentهای Redacted.
راهنمای ساخت Pipeline تست خودکار و مقاله تست مداوم در DevOps طراحی Gateها را کامل میکنند.
هر گزارش باید Context اجرا را حمل کند
- Commit SHA، Build و نسخه Artifact؛
- نسخه Cucumber، Runtime و Pluginها؛
- Environment، Adapter/Profile و Base URL غیرحساس؛
- Tag Expression و تعداد Scenario انتخاب/حذفشده؛
- Locale، Timezone، Random Seed و Parallelism؛
- Retry count و نتیجه هر تلاش، نه فقط آخرین سبزی؛
- زمان شروع/پایان و Correlation ID؛
- Failure/Attachment کمینه و Redacted.
Retry درمان Flake نیست
Retry برای جمعآوری شاهد یا تحمل یک Dependency با سیاست صریح ممکن است مفید باشد، اما گزارش باید هر تلاش را نشان دهد. Scenario که بار اول Fail و بار دوم Pass شده «پایدار» نیست. آن را با مالک، علت اولیه و SLA رفع ثبت کنید؛ Quarantine نباید Gate بحرانی را برای همیشه دور بزند.
مدیریت نسخه و تفاوت Runtimeها
- ماژولهای Cucumber یک Runtime را روی نسخه سازگار Lock کنید؛
- JUnit/TestNG/JS Runner و Formatter را جداگانه Compatibility-check کنید؛
- قبل از Upgrade، Parse، Glue contract و یک Canary Suite را اجرا کنید؛
- تغییر وضعیت Step، Hook ordering، Parallel granularity و Report format را بررسی کنید؛
- Dependency Bot را مستقیم به Merge خودکار Test Infrastructure وصل نکنید؛
- Runbook Rollback و نسخه Artifact قبلی را نگه دارید.
شماره نسخهای که امروز در مثال رسمی دیده میشود نباید در معماری Hard-code شود. Dependency Management منبع واحد نسخه باشد و مقاله/README پروژه به مستندات همان Major پیوند دهد.
مثال ایرانی: Suite تخفیف و بازپرداخت
فرض کنید مارکتپلیس ایرانی Rule تخفیف و بازپرداخت دارد. Formulation فارسی در مقاله ۱۱۰۰ انجام شده است؛ اینجا معماری اجرا را میچینیم:
features/billing/discounts.featureRule سقف تخفیف را نگه میدارد؛features/billing/refunds.featureRule بازپرداخت پیش/پس از تسویه را نگه میدارد؛BillingStepsعبارتهای سفارش، تخفیف و بازپرداخت را بین Featureها بازاستفاده میکند؛{rial}ارقام فارسی/لاتین و جداکننده را به Money ریال تبدیل میکند؛ScenarioContextفقط DraftOrder، Quote و RefundResult همان Scenario را دارد؛BillingDriverPort مشترک وApiBillingDriver/UiBillingDriverAdapter هستند؛PspStubSuccess، Timeout، Callback تکراری و Status نامعتبر را شبیهسازی میکند؛@external-pspفقط Contract/Canary محدود Sandbox را انتخاب میکند؛- Order ID با Run ID یکتا و Cleanup دارای TTL است؛
- Report رقم حساب، موبایل، Token و Payload خام PSP را Mask میکند.
پروفایلهای اجرا
- PR: In-process/API + PSP Stub، بدون Browser؛
- Integration: API + Database/Queue واقعی محیط تست؛
- Critical UI: تعداد کم مسیر ریال/تومان و نمایش خطا؛
- Nightly Persian: ارقام فارسی/لاتین، نیمفاصله، RTL، جلالی و Tehran Time؛
- External Canary: سهمیه محدود PSP Sandbox با داده مصنوعی.
این تفکیک به معنی رفتار متفاوت نیست؛ Adapter و دامنه شاهد متفاوتاند. نتیجه هر Profile باید معلوم کند کدام Risk را پوشش داده و چه Dependencyهایی Stub بودهاند.
مهاجرت از Suite بههمریخته بدون بازنویسی یکباره
- Inventory: Feature، Step، Hook، Tag، Runtime، Adapter و زمان اجرا را فهرست کنید؛
- Freeze vocabulary: واژههای دامنه و Stepهای مترادف را مشخص کنید؛
- Detect: Undefined، Ambiguous، Step استفادهنشده و Feature-coupled را پیدا کنید؛
- Extract drivers: HTTP/UI/SQL را از Step به Driver/Adapter منتقل کنید؛
- Scope state: Static و Singleton را به Scenario Context/DI تبدیل کنید؛
- Audit hooks: شرط کسبوکار را به Background و Lifecycle فنی را به Hook کمینه ببرید؛
- Register tags: Taxonomy، مالک و Quarantine expiry تعریف کنید؛
- Split profiles: Port/API، Integration و Full-stack را جدا کنید؛
- Prove isolation: ترتیب تصادفی، تکرار و ۲ Worker را امتحان کنید؛
- Ratchet: برای کد جدید Rule سختتر و برای Legacy بودجه کاهش تدریجی بگذارید.
Big-bang migration معمولاً اعتماد به Suite را کمتر میکند. یک Bounded Context با درد و ارزش بالا انتخاب کنید و Baseline زمان، Flake و هزینه تشخیص را پیش از تغییر ثبت کنید.
معیارهای سلامت Suite بدون بازی با اعداد
- Glue correctness: Undefined/Ambiguous/Pending در Branch اصلی باید صفر باشد؛
- Stability: First-attempt pass و Flake بر اساس Scenario/Adapter؛
- Feedback: p50/p95 زمان PR و Scheduled Suite؛
- Diagnosis: زمان از Failure تا تعیین Component/مالک؛
- Isolation: اختلاف نتیجه تنها، ترتیبی و موازی؛
- Maintenance: تعداد Feature/Step تغییرکرده برای یک Rule یا UI Change؛
- Quarantine: تعداد، سن، مالک و نرخ بازگشت؛
- Hook cost: سهم Setup/Teardown از کل Runtime؛
- Portfolio: نسبت Port/API/Full-stack بر اساس Risk، نه هدف درصدی ثابت؛
- Evidence: اجراهای دارای Context کامل و Attachment امن.
تعداد Step Reuse بالا میتواند حاصل Regex عمومی و بد باشد؛ تعداد فایل کم میتواند Monolith پنهان باشد؛ Parallel speedup میتواند با فشار به Environment خریده شود. هر سنجه را با Guardrail و نمونه کد واقعی بازبینی کنید.
برنامه ۳۰روزه اصلاح معماری Cucumber
هفته اول: قرارداد و Baseline
- یک Domain پرارزش و ۱۰ تا ۲۰ Scenario نماینده انتخاب کنید؛
- درخت وابستگی، Tagها، Hookها و State سراسری را ثبت کنید؛
- زمان، Flake و وضعیت Undefined/Ambiguous را Baseline بگیرید.
هفته دوم: Glue و State
- Stepها را بر اساس مفهوم دامنه گروهبندی کنید؛
- Cucumber Expression و Typeهای دامنه بسازید؛
- Context Scenario-scoped و Driver Interface را استخراج کنید.
هفته سوم: Hook، Tag و Adapter
- Hookهای پنهان/زنجیرهای را کم و Lifecycle را صریح کنید؛
- Tag Registry و Quarantine expiry را اعمال کنید؛
- Ruleهای سریع را از UI به Port/API Adapter منتقل کنید.
هفته چهارم: Parallel، CI و Review
- Isolation را با ترتیب تصادفی و دو Worker اثبات کنید؛
- Context اجرا و Retry history را به Artifact اضافه کنید؛
- Before/After metric را مقایسه و Ratchet ماه بعد را تعیین کنید.
ضدالگوهای Cucumber که باید متوقف شوند
- یک کلاس Step برای هر Feature File؛
CommonStepsبدون مرز دامنه؛- DRY افراطی و Expressionهایی که هر متن را Match میکنند؛
- فراخوانی Step از داخل Step Definition؛
- منطق کسبوکار، HTTP، SQL یا Selenium مستقیم در Glue؛
- کپی فرمول Expected از کد محصول؛
- Page Object بهعنوان کل معماری تست؛
- Static/Singleton State و Session مشترک Scenarioها؛
- شرط کسبوکار پنهان در Hook؛
- اتکا به ترتیب چند Hook برای درستبودن تست؛
- Tagهای بیمالک، ترکیبی یا Quarantine بیانقضا؛
- اجرای همه Scenarioها از UI؛
- Parallel نامحدود بدون سنجش ظرفیت؛
- سبزکردن Flake با Retry پنهان؛
- انتشار Screenshot/Payload حساس در Report عمومی.
چکلیست Pull Request برای Suite Cucumber
- Feature در Bounded Context درست و با Rule منسجم است.
- Step جدید مفهوم دامنه دارد و Feature-coupled نیست.
- Expression یکتا و Type ورودی محدود و تستشده است.
- Glue فقط Driver/Context/Assertion را هماهنگ میکند.
- Expected از مثال مستقل میآید، نه کپی منطق محصول.
- State فقط Scenario-scoped و بدون Static mutable است.
- Hook فقط Lifecycle فنی و Cleanup آن Idempotent است.
- Tag در Registry وجود دارد و Quarantine انقضا دارد.
- Adapter/سطح اجرا با Risk متناسب است.
- داده یکتا، مصنوعی و پاکسازی Crash-safe است.
- Execution تنها/موازی نتیجه یکسان دارد.
- Report Context کامل و هیچ Secret/PII ندارد.
پرسشهای متداول معماری Cucumber
بهترین ساختار پوشه Cucumber چیست؟
ساختار واحدی برای همه Runtimeها وجود ندارد. Featureها را بر اساس Bounded Context/Capability و Glue را بر اساس مفاهیم دامنه بچینید؛ Type، Context، Driver، Adapter، Fixture و Hook را از Step جدا کنید. جهت وابستگی و Cohesion مهمتر از نام دقیق پوشه است.
Background چه تفاوتی با Before Hook دارد؟
Background زمینه کسبوکاری مشترک و قابل مشاهده برای خواننده Feature است. Before Hook آمادهسازی فنی مانند Driver، Trace یا داده زیرساخت را انجام میدهد. اگر حذف Hook معنای Rule را برای خواننده عوض میکند، احتمالاً پیششرط کسبوکار را پنهان کردهاید.
State را بین Step Definitionها چگونه به اشتراک بگذاریم؟
فقط در محدوده همان Scenario. در JVM میتوان Context تایپشده را با DI و Scenario Scope تزریق کرد؛ در JavaScript، World و در Runtimeهای دیگر سازوکار متناظر وجود دارد. Static، Singleton یا Global mutable باعث نشت State و شکست Parallel میشود.
آیا همه Stepها باید قابل استفاده مجدد باشند؟
خیر. هدف، زبان دامنه منسجم و Glue قابل نگهداری است. Reuse وقتی خوب است که دو عبارت واقعاً یک مفهوم دارند. عمومیسازی صرفاً برای بالا بردن درصد Reuse میتواند Expression مبهم و Step بیمعنا بسازد.
چگونه Cucumber را موازی کنیم بدون Flaky Test؟
ابتدا State سراسری را حذف، داده/Resource را یکتا، Clock و Random را کنترل و Cleanup را Idempotent کنید. نتیجه اجرای تنها و ترتیبی را با دو Worker مقایسه کنید؛ سپس تعداد Worker را با توجه به Connection Pool و سهمیه وابستگی مرحلهای افزایش دهید. Parallelism علت Flake را رفع نمیکند، آن را آشکار میکند.
جمعبندی
بهترین شیوه Cucumber یک فهرست ثابت از نام پوشه و Annotation نیست. معماری خوب، Specification دامنه را از Glue و ابزار اجرا جدا میکند: Feature حول Capability، Step حول مفهوم دامنه، Expression یکتا، Context محدود به Scenario، Driver مستقل از Cucumber و Adapter متناسب با ریسک. Hook فقط Lifecycle فنی و Tag فقط Taxonomy کوچک و قابل ممیزی است.
پایداری Suite با تعداد Scenario یا درصد Reuse سنجیده نمیشود. صفر Undefined/Ambiguous، First-attempt stability، Feedback سریع، Isolation واقعی، Quarantine کوتاهعمر و گزارش امن/قابل بازتولید سیگنالهای بهتریاند. این اصول را روی یک Bounded Context اجرا، Baseline را مقایسه و سپس بهتدریج به Legacy گسترش دهید.

