یک تست پرداخت در CI قرمز شده است؛ اما معلوم نیست خطا از منطق مبلغ است، اختلاف ریال و تومان، پاسخ درگاه، ساعت سیستم، داده باقیمانده از اجرای قبلی یا خود تست. تیم میتواند همان تست را ده بار دیگر اجرا کند، ولی هنوز شواهدی برای تصمیم ندارد. این مشکل کمبود «تست» نیست؛ مشکل، تستپذیری نرمافزار است.
سیستم تستپذیر به تیم اجازه میدهد یک سؤال مشخص را به شرایط قابلکنترل، شواهد قابلمشاهده و یک حکم قابلاعتماد تبدیل کند. این مقاله نشان میدهد چگونه تستپذیری را از یک صفت مبهم به قرارداد معماری، معیار قابلاندازهگیری و برنامه بهبود تبدیل کنیم؛ از کد واحد تا سرویس توزیعشده و محیط واقعی.
تستپذیری نرمافزار چیست؟
تستپذیری یا Testability میزان سهولت و هزینهای است که با آن میتوان درباره یک ویژگی یا ریسک نرمافزار، در یک سطح و محیط مشخص، شواهد معتبر به دست آورد. عبارتهای «یک ویژگی»، «یک سطح» و «یک محیط» مهماند: یک تابع محاسبه تخفیف ممکن است در تست واحد بسیار تستپذیر باشد، ولی رفتار کل سفارش در برابر callback تکراری درگاه فقط در سطح یکپارچهسازی قابلارزیابی باشد.
مدل کیفیت محصول ISO/IEC ۲۵۰۱۰:۲۰۲۳ ویژگیها و زیرویژگیهایی برای مشخصکردن، اندازهگیری و ارزیابی کیفیت محصول ارائه میکند. ترجمه عملی این نگاه برای تیم محصول این است که «تستپذیر» باید به هدف، معیار و شواهد متصل شود؛ نه اینکه صرفاً برچسبی مثبت برای معماری باشد.
تستپذیری نسبی است، نه صفر و یک
یک API ممکن است برای بررسی status code عالی باشد، اما برای اثبات ثبت صحیح سند مالی ضعیف باشد. یک Fake سریع میتواند منطق retry را کنترلپذیر کند، ولی ناسازگاری واقعی با قرارداد PSP را نبیند. بنابراین جمله «این سیستم تستپذیر است» ناقص است. جمله بهتر چنین است:
«برای ریسک دوبارهبرداشتشدن مبلغ، در سطح سرویس پرداخت، میتوانیم timeout و callback تکراری را کنترل کنیم، اثر را در دفترکل مشاهده کنیم و با invariant یکتایی تراکنش نتیجه را داوری کنیم.»
چهار مفهوم را با تستپذیری یکی نگیریم
- Testing: فعالیت طراحی و اجرای آزمایش است؛ Testability ویژگی سیستم و محیطی است که این فعالیت را آسان یا دشوار میکند.
- Test automation: ماشینیکردن اجراست. یک suite خودکار میتواند کند، ناپایدار و بیتشخیص باشد و همچنان تستپذیری پایینی نشان دهد.
- Observability: از خروجیهای بیرونی، اطلاعاتی درباره وضعیت درونی میدهد؛ اما log، metric و trace بهتنهایی نمیگویند نتیجه درست است. برای حکم به Oracle نیاز داریم. راهنمای OpenTelemetry سیگنالهای telemetry را توضیح میدهد، اما این سیگنالها مواد خام شواهدند، نه اثبات صحت کسبوکار.
- نگهداشتپذیری کد تست: خوانایی و تغییرپذیری suite مهم است، ولی موضوع جداگانهای دارد. برای الگوی Fixture، Page Component و Test Double به راهنمای کد تست قابل نگهداری مراجعه کنید.
مدل هشتبخشی طراحی برای تستپذیری
این زنجیره را برای هر ریسک طی کنید:
سؤال/ریسک ← کنترل ← مشاهده ← Oracle ← ایزولاسیون/بازنشانی ← بازتولید ← تشخیص ← سنجش
۱. سؤال یا ریسک
آزمایش قرار است چه عدمقطعیتی را کم کند؟ «پرداخت کار میکند» سؤال مناسبی نیست. «اگر PSP پس از ثبت پرداخت timeout بدهد و همان callback دوباره برسد، آیا سفارش فقط یک بار پرداختشده میشود؟» قابلآزمایش است.
۲. کنترلپذیری
آیا تست میتواند ورودی، زمان، تصادف، پاسخ dependency، خطا، مجوز، شبکه و ترتیب رویداد را به شکل مجاز تنظیم کند؟ اگر تنها راه دیدن timeout صبرکردن برای خرابی واقعی PSP باشد، کنترلپذیری پایین است.
۳. مشاهدهپذیری آزمون
آیا اثر مهم را میبینیم؟ status نهایی کافی نیست اگر باید ledger، outbox، audit event یا attemptهای retry را نیز بررسی کنیم. شواهد باید به یک شناسه اجرای یکتا و correlation ID متصل باشد.
۴. Oracle
Oracle سازوکاری است که نتیجه واقعی را با انتظار مقایسه میکند. انتظار نباید صرفاً همان الگوریتم محصول را دوباره پیاده کند. invariant کسبوکار، قرارداد منتشرشده، محاسبه مستقل یا داده مرجع میتواند Oracle باشد.
۵. ایزولاسیون و بازنشانی
هر اجرا باید namespace، tenant، داده و منابع مشخص داشته باشد. بازنشانی لزوماً به معنای حذف کل پایگاه داده نیست؛ transaction rollback، schema مجزا، داده نسخهدار یا TTL نیز ممکن است مناسب باشد. طراحی جزئیات این بخش در راهنمای مدیریت داده تست آمده است.
۶. بازتولیدپذیری
commit، image digest، configuration، schema، seed، timezone، locale، dependency mode و ترتیب رخدادها باید ثبت شوند. «روی سیستم من پاس شد» بدون هویت اجرا، شواهد قابلمقایسهای نیست.
۷. تشخیصپذیری
شکست باید حداقل به Product، Test، Data، Environment، Dependency یا Unknown طبقهبندی شود. پیام «expected ۲۰۰, got ۵۰۰» بدون request ID، state transition و پاسخ dependency برای تشخیص کافی نیست.
۸. سنجش
باید بدانیم گرفتن شواهد چقدر زمان و هزینه دارد و چند بار در تلاش اول همان نتیجه بازتولید میشود. در ادامه معیارهای عملی را تعریف میکنیم.
از ریسک شروع کنید، نه از الگوی معماری
Dependency Injection، Interface یا Mock هدف نیستند. ابتدا سؤال مهم را انتخاب کنید و سپس کوچکترین seam لازم برای کنترل یا مشاهده آن را بسازید.
| ریسک | کنترل لازم | شاهد | Oracle |
|---|---|---|---|
| محاسبه اشتباه مبلغ | قیمت، تخفیف، واحد پول و rounding | ریز محاسبه و مبلغ درخواست PSP | جدول نمونه مستقل و invariant مجموع |
| ثبت دوباره callback | تعداد و ترتیب callbackها | تغییر وضعیت، ledger و outbox | حداکثر یک اثر مالی برای کلید idempotency |
| خطای مرز روز | Clock و timezone | timestampهای UTC و زمان کسبوکار | قاعده تاریخ سررسید |
| قطع dependency | timeout، ۴۲۹، ۵۰۰ و پاسخ ناقص | attempt، backoff و نتیجه نهایی | سیاست retry و عدم تکرار side effect |
| نشت داده | payload دارای داده حساس مصنوعی | log، trace و artifact | قاعده redact و فهرست فیلدهای ممنوع |
الگوهای معماری که واقعاً کمک میکنند
وابستگیهای ناپایدار را پشت seam کوچک قرار دهید
Clock، مولد شناسه، تصادف، PSP، پیامک، فایل و queue را از منطق تصمیم جدا کنید. seam میتواند یک پارامتر تابع یا شیء کوچک باشد؛ لازم نیست برای هر کلاس یک Interface و container پیچیده بسازید. راهنمای رسمی Dependency Injection در .NET نیز هشدار میدهد که container جایگزین طراحی روشن وابستگی و مدیریت طولعمر نیست.
State transition را صریح کنید
بهجای چند boolean پراکنده، transitionهای مجاز مانند Pending → Verified → Paid را تعریف و ثبت کنید. سپس تست میتواند ورودی، حالت پیشین، رخداد و حالت بعدی را ببیند. transition نامعتبر باید خطای مشخص بدهد، نه اینکه بیصدا نادیده گرفته شود.
اثر جانبی را از تصمیم جدا کنید
محاسبه «آیا retry مجاز است؟» را تا حد ممکن خالص نگه دارید و ارسال درخواست را به adapter بسپارید. این جداسازی هم تست سریع منطق را ممکن میکند و هم اجازه میدهد adapter واقعی با contract test یا sandbox بررسی شود.
نتیجه میانیِ معنادار تولید کنید
کد برای تست نباید internals بیارزش را public کند؛ اما رخداد دامنه، audit record، reason code و شناسه عملیات بخشی از قابلیت عملیاتی سالماند. این خروجیها هم به تست و هم به پشتیبانی تولید کمک میکنند.
مثال کد: زمان و درگاه را قابلکنترل کنید
کد زیر به syntax نزدیک TypeScript نوشته شده است. نکته، Framework نیست؛ صریحکردن وابستگیهای volatile است:
type Clock = { now(): Date };
type PaymentPort = {
verify(input: { authority: string; amountRial: number }):
Promise<{ ok: boolean; referenceId?: string }>;
};
class CompletePayment {
constructor(
private readonly clock: Clock,
private readonly psp: PaymentPort,
private readonly orders: OrderRepository,
) {}
async execute(orderId: string, authority: string) {
const order = await this.orders.get(orderId);
if (order.isPaid()) return order.result(); // idempotent outcome
const verified = await this.psp.verify({
authority,
amountRial: order.payableRial,
});
if (!verified.ok) return order.reject("PSP_NOT_VERIFIED");
return order.markPaid({
referenceId: verified.referenceId!,
paidAt: this.clock.now(),
});
}
}
در تست میتوان Clock ثابت و PSP برنامهپذیر تزریق کرد، callback را دوباره فرستاد و invariant یکتایی اثر را بررسی کرد. در تست واقعی adapter، همان قرارداد با sandbox یا محیط کنترلشده PSP سنجیده میشود. راهنمای Clock در Playwright نمونهای از کنترل زمان در مرورگر است؛ بااینحال ساعت backend، پایگاه داده و scheduler نیز باید در سطح مناسب کنترل یا ثبت شوند.
قرارداد تستپذیری؛ Definition of Done قابلبررسی
برای قابلیتهای پرریسک، یک Testability Contract کوتاه کنار acceptance criteria نگه دارید. این قرارداد تستکیس نیست؛ پیشنیاز تولید شواهد معتبر را تعریف میکند.
| فیلد | پرسش قراردادی | نمونه پرداخت |
|---|---|---|
| Risk/Question | چه عدمقطعیتی را کم میکنیم؟ | callback تکراری اثر مالی دوم نسازد |
| Control | چه شرایطی قابل تنظیم است؟ | پاسخ PSP، timeout، Clock و ترتیب رخداد |
| Evidence | چه خروجیهایی و با چه شناسهای ثبت میشود؟ | order، payment attempt، ledger، outbox و trace ID |
| Oracle | حکم مستقل چیست؟ | یک reference و یک credit برای idempotency key |
| Isolation/Reset | داده و منابع چگونه جدا و پاک میشوند؟ | tenant اجرای یکتا و TTL رکورد مصنوعی |
| Run identity | برای بازتولید چه چیزی ثبت میشود؟ | commit، config hash، schema، seed و timezone |
| Budget | حد زمان و هزینه شواهد چیست؟ | نتیجه component زیر ۶۰ ثانیه؛ E2E زیر ۸ دقیقه |
| Safety | چه سوءاستفادهای باید مسدود شود؟ | عدم استفاده از کارت واقعی؛ RBAC، audit و rate limit |
| Owner | شکست مبهم را چه کسی triage میکند؟ | مالک سرویس پرداخت با همراهی QE |
در چرخه اجرا، این قرارداد باید به هویت build، environment، data و نتیجه متصل شود؛ راهنمای مدیریت Test Run مدل ثبت شواهد و معنای وضعیتها را پوشش میدهد.
سناریوی کامل ایرانی: پرداخت فروشگاه
فرض کنید کاربر کالایی به قیمت ۱۲۵٬۰۰۰ تومان میخرد و API درگاه مبلغ را به ریال میخواهد. PSP درخواست را دریافت میکند، ولی پاسخ verify به علت timeout به برنامه نمیرسد؛ چند ثانیه بعد نیز callback دوبار ارسال میشود. ساعت سرور UTC است و گزارش مالی بر مبنای Asia/Tehran بسته میشود.
کنترلها
- Clock تزریقی برای قبل و بعد از مرز روز؛
- PSP Fake سناریومحور برای timeout، تأیید، رد، ۴۲۹ و پاسخ ناقص؛
- ارسال callback یکسان و callback با authority متفاوت؛
- ورودی با ارقام فارسی و انگلیسی، و مبلغ صریح با واحد IRR؛
- شناسه tenant/run یکتا و reset قابلتکرار.
شواهد و Oracle
- مبلغ ارسالی باید دقیقاً ۱٬۲۵۰٬۰۰۰ ریال باشد؛
- بدون verify معتبر، سفارش به Paid نرود؛
- برای یک idempotency key بیش از یک اثر مالی ثبت نشود؛
- ledger، order و event منتشرشده به یک reference متصل باشند؛
- timestamp خام UTC و تاریخ کسبوکار تهران قابلردیابی باشند؛
- شماره کارت، رمز، token کامل و داده شخصی در log یا artifact دیده نشود.
Sandbox فقط بخشی از مسئله را حل میکند و ممکن است همه خطاهای شبکه، قرارداد یا رفتار محیط واقعی را بازنمایی نکند. راهنمای تست درگاه پرداخت را بهعنوان لایه مکمل در نظر بگیرید، نه Oracle یگانه.
ایزولاسیون فقط پاککردن Browser نیست
BrowserContext در Playwright cookie و storage سمت مرورگر را برای هر تست جدا میکند؛ اما سفارش، cache، queue، فایل و پیامک backend همچنان ممکن است مشترک باشند. ایزولاسیون کامل را در سطوح زیر بررسی کنید:
- Identity: tenant، user، order و idempotency key یکتا؛
- Data: schema/namespace یا مالکیت روشن رکوردها؛
- Messaging: topic، consumer group و correlation اختصاصی؛
- Cache: prefix و TTL اجرای یکتا؛
- Files/artifacts: مسیر، retention و دسترسی مجزا؛
- External dependency: حساب sandbox یا virtual service قابلشناسایی.
اگر محیط قابلتولید و پاکسازی ندارید، معماری اجرایی Docker Compose برای محیط تست نقطه شروع مناسبی است؛ Container بهتنهایی تضمین deterministic بودن داده و dependency نیست.
Oracle مستقل بسازید؛ فقط response را assert نکنید
ضعیفترین Oracle معمولاً همان چیزی را بررسی میکند که پیادهسازی بهسادگی تولید کرده است. برای افزایش قدرت داوری:
- invariant دامنه را از جزئیات پیادهسازی جدا کنید؛
- محاسبات حساس را با جدول نمونه مستقل یا implementation دوم کوچک کنترل کنید؛
- در تست API، علاوه بر response، اثر پایدار یا event را در سطح مناسب ببینید؛
- برای قرارداد dependency، success و error schema را هر دو بسنجید؛
- expected value را از همان تابع production محاسبه نکنید.
راهنمای API Testing در Playwright نشان میدهد چگونه میتوان API را مستقیم آزمود یا state لازم برای تست UI را ساخت. این مسیر میتواند setup را سریعتر کند، مشروط بر اینکه از همان نقص منطقیِ مسیر UI عبور نکند و دادهای بدون مالکیت باقی نگذارد.
سیستمهای توزیعشده و همزمان را چگونه تستپذیر کنیم؟
ترتیب رخداد را کنترل و ثبت کنید
برای race condition، صرفاً اجرای موازی کافی نیست. barrier، latch، scheduler کنترلشده یا نقطه هماهنگی مخصوص محیط تست میتواند interleaving هدف را تکرار کند. هر hook باید محدود، احراز هویتشده و در production غیرفعال یا سختمحافظتشده باشد.
زمان منطقی را از زمان دیواری جدا کنید
timestamp برای مشاهده ترتیب کافی نیست؛ در سیستم توزیعشده clock skew و تأخیر شبکه وجود دارد. version، sequence، causation ID و idempotency key اغلب شواهد بهتری برای رابطه رخدادها هستند.
Failure Injection را بودجهدار کنید
delay، reset connection، پاسخ ناقص یا قطعی dependency را در محیط مجاز تزریق کنید. دامنه، مدت، blast radius، مالک و شرط توقف باید روشن باشد. برای لَب بازتولید و جمعآوری Evidence Pack، راهنمای عیبیابی با Docker Compose مفید است.
تستپذیری در Production بدون ساخت Backdoor
بعضی رفتارها فقط با runtime واقعی، تنظیمات واقعی و ترافیک واقعی آشکار میشوند. فصل Testing for Reliability از Google SRE میان تستهای سنتی و تستهای production تمایز میگذارد و تأکید میکند پاسشدن تست لزوماً قابلیت اطمینان را اثبات نمیکند. برای probe یا آزمایش کنترلشده در production این guardrailها را اعمال کنید:
- tenant و داده مصنوعیِ قابلشناسایی با TTL؛
- عدم استفاده از کارت، شماره همراه یا هویت واقعی؛
- RBAC، audit trail، allowlist و rate limit برای endpointهای کنترلی؛
- kill switch، بودجه خطا و blast radius محدود؛
- تفکیک telemetry کاربر واقعی از ترافیک مصنوعی؛
- حذف یا مسدودسازی قطعی قابلیتهای خطرناک در build عمومی.
یک endpoint مخفی که با query string هر نتیجهای میسازد، testability نیست؛ آسیبپذیری است. تستپذیری باید با مدل تهدید و الزامات حریم خصوصی همزمان طراحی شود.
Test Double مفید است، اما واقعیت را جایگزین نمیکند
Stub یا Fake برای تولید خطاهای کمیاب و اجرای سریع عالی است، اما ممکن است از dependency واقعی drift کند. یک راهبرد متعادل سه لایه دارد:
- تست سریع منطق با double کوچک و رفتار صریح؛
- contract test برای همترازی consumer و provider یا adapter واقعی؛
- تعداد محدود تست sandbox/E2E برای TLS، header، encoding، timeout و رفتار عملیاتی.
هر Fake باید مالک، نسخه قرارداد، تاریخ اعتبار و تست کالیبراسیون داشته باشد. اگر Fake از محصول پیچیدهتر شده است، احتمالاً boundary یا سطح تست اشتباه انتخاب شده است.
تستپذیری سیستم Legacy؛ اول حیاتیترین سؤال
در کد قدیمی، تلاش برای Unit Test کردن همه چیز معمولاً پروژهای طولانی و کمبازده میسازد. راه کمریسکتر:
- یک جریان درآمدی، امنیتی یا پرتغییر را انتخاب کنید؛
- رفتار مهم را از بیرون با characterization test ثبت کنید؛
- برای Clock، شبکه یا storage یک seam باریک بسازید؛
- شواهد failure را قبل از refactor تقویت کنید؛
- بخش تصمیمگیری را قدمبهقدم از اثر جانبی جدا کنید؛
- در هر گام، تست سطح بالاتر را بهعنوان safety net نگه دارید.
Characterization test فقط رفتار موجود را ثبت میکند؛ درستبودن آن رفتار را تضمین نمیکند. خروجی طلایی میتواند باگ یا داده حساس را منجمد کند. پیش از تبدیل آن به Oracle دائمی، رفتار را با نیاز کسبوکار و نمونه مستقل مرور کنید.
هزینهها و Trade-offهای طراحی برای تستپذیری
| انتخاب | فایده | ریسک | کنترل |
|---|---|---|---|
| Interface/seam | کنترل dependency | انفجار abstraction | فقط در مرز volatile یا ریسکدار |
| Fake | سرعت و سناریوی خطا | drift از واقعیت | contract و کالیبراسیون دورهای |
| Test hook | کنترل رخداد دشوار | سوءاستفاده امنیتی | RBAC، audit، build flag و حذف در production |
| Telemetry بیشتر | تشخیص بهتر | هزینه، نویز و نشت PII | schema، sampling، redact و retention |
| محیط کاملاً hermetic | تکرارپذیری | کاهش fidelity | لایه مکمل با dependency واقعی |
هدف «بیشترین تستپذیری ممکن» نیست؛ هدف کمهزینهترین شواهد کافی برای ریسک است.
معیارهای سنجش تستپذیری
تعداد test case یا درصد coverage، معیار مستقیم تستپذیری نیست. این مجموعه را برای یک جریان مشخص و به تفکیک سطح تست بسنجید:
- Setup lead time: میانه و صدک ۹۵ زمان آمادهشدن state؛
- Time to evidence: از commit یا درخواست اجرا تا Evidence Pack قابلداوری؛
- First-run reproducibility: درصد شکستهایی که با همان هویت اجرا در تلاش اول تکرار میشوند؛
- Dependency controllability: سهم سناریوهای ریسک که success، timeout و failure dependency آنها قابلکنترل است؛
- Reset reliability: درصد cleanupهای موفق بدون داده یا resource باقیمانده؛
- Oracle independence: سهم سناریوهای حیاتی با حکم مستقل از implementation؛
- Evidence completeness: درصد شکستها با run ID، input، config، state transition و dependency evidence کامل؛
- Median classification time: زمان تا انتساب اولیه Product/Test/Data/Environment/Dependency؛
- Safe probe coverage: سهم جریانهای بحرانی که probe کمخطر و auditable دارند.
عددها را به هدف کسبوکار و روند زمانی متصل کنید. کاهش زمان تشخیص با افزایش نشت داده، موفقیت نیست؛ افزایش controllability با Fake ناسازگار نیز شاخصی فریبنده است.
پرسشهای بازبینی معماری
- سه ریسک اصلی این قابلیت چیست و هر کدام در چه سطحی باید آزموده شود؟
- کدام زمان، تصادف، dependency، permission و failure قابلکنترل نیست؟
- آیا شاهد رفتار مهم را میبینیم یا فقط response ظاهری را؟
- Oracle از implementation مستقل است؟
- تست موازی چگونه داده، cache، queue و فایل را جدا میکند؟
- برای بازتولید شکست چه هویت و artifactی ذخیره میشود؟
- Failure مبهم در چند دقیقه و توسط چه مالکی triage میشود؟
- Fake چگونه با dependency واقعی کالیبره میشود؟
- Test hook چه تهدید امنیتی یا حریم خصوصی میسازد؟
- کدام abstraction فقط برای تست ساخته شده و آیا ارزشش از هزینهاش بیشتر است؟
برنامه ۳۰روزه بهبود تستپذیری
هفته اول: خط مبنا
یک جریان حیاتی انتخاب کنید؛ پنج شکست اخیر را دستهبندی کنید؛ زمان setup، بازتولید و تشخیص را بسنجید؛ Contract اولیه را بنویسید. همه سیستم را همزمان هدف نگیرید.
هفته دوم: کنترل و ایزولاسیون
Clock و یک dependency پرخطا را پشت seam کوچک ببرید. run/tenant ID یکتا، seed نسخهدار و cleanup قابلمشاهده اضافه کنید.
هفته سوم: شواهد و Oracle
correlation، transition و dependency attempt را بدون داده حساس ثبت کنید. invariantهای دامنه و expected dataset مستقل بسازید؛ یک failure مهم را عمداً تزریق و Evidence Pack را مرور کنید.
هفته چهارم: Pipeline و سیاست
تست سریع را در PR، تست dependency را در lane کندتر و probe مجاز را پس از استقرار قرار دهید. مالک triage، budget زمانی، quarantine انقضادار و dashboard روند را مشخص کنید. برای جداسازی lane و gate از راهنمای Continuous Testing در CI/CD استفاده کنید و در GitLab، هویت اجرا و Artifact را مطابق راهنمای تست خودکار در GitLab CI ثبت کنید.
ضدالگوهای رایج
- اضافهکردن Interface برای هر کلاس، بدون ریسک یا نقطه تغییر واقعی؛
- Mock کردن همه dependencyها و حذف هرگونه تست قرارداد؛
- استفاده از retry برای پنهانکردن شکست غیرقابلبازتولید؛
- اتکا به log حجیم بدون correlation، schema و retention؛
- تولید expected value با همان تابع production؛
- اشتراک tenant، queue یا cache میان تستهای موازی؛
- استفاده از sleep بهجای شرط readiness یا Clock کنترلشده؛
- نگهداری token، شماره کارت یا PII در screenshot و artifact؛
- endpoint مخفی و بدون audit برای تغییر state در production؛
- یکیگرفتن coverage بالا با شواهد کافی یا کیفیت محصول؛
- ثبت snapshot طلایی بدون بررسی اینکه رفتار موجود درست است؛
- سنجهای که با بیشترکردن تستهای کمارزش بهسادگی بازی داده میشود.
چکلیست نهایی Testability Contract
- ریسک و سؤال آزمون دقیق و سطح تست انتخاب شده است.
- زمان، تصادف، dependency و failureهای مهم قابلکنترلاند.
- اثر کسبوکار و state transition با run ID دیده میشود.
- Oracle مستقل و معنای pass/fail روشن است.
- داده، cache، queue و artifact میان اجراها جدا هستند.
- commit، config، image، schema، seed، locale و timezone ثبت میشوند.
- شکست به یک دسته و مالک triage میرسد.
- Fake با قرارداد یا dependency واقعی کالیبره میشود.
- hook و telemetry از نظر امنیت، PII، دسترسی و retention بازبینی شدهاند.
- زمان و هزینه شواهد متناسب با ریسک است.
پرسشهای متداول درباره تستپذیری نرمافزار
آیا کدی که Unit Test دارد حتماً تستپذیر است؟
خیر. Unit Test ممکن است فقط یک تابع جدا را پوشش دهد، درحالیکه ریسک اصلی در پایگاه داده، queue، قرارداد بیرونی یا concurrency باشد. تستپذیری باید نسبت به سؤال و سطح موردنیاز سنجیده شود.
آیا Dependency Injection همیشه تستپذیری را بهتر میکند؟
فقط وقتی وابستگی مهمی را صریح و قابلکنترل کند. تزریق بیهدف میتواند abstraction، wiring و هزینه نگهداری را بالا ببرد. ابتدا ریسک و seam لازم را مشخص کنید.
تفاوت Observability و Testability چیست؟
Observability به دیدن و استنباط وضعیت کمک میکند؛ Testability علاوه بر مشاهده، به کنترل شرایط، Oracle، ایزولاسیون، بازتولید و هزینه شواهد وابسته است. telemetry خوب بدون حکم مستقل ممکن است فقط خطای مبهم را پرجزئیاتتر کند.
برای سیستم Legacy از کجا شروع کنیم؟
از حیاتیترین یا پرتغییرترین جریان، نه کل codebase. ابتدا behavior مهم را در مرز سیستم ثبت کنید، سپس یک وابستگی ناپایدار را قابلکنترل و failure evidence را قابلتشخیص کنید.
بهترین KPI تستپذیری چیست؟
یک KPI یگانه وجود ندارد. برای شروع، Time to Evidence، بازتولید در تلاش اول و زمان دستهبندی شکست را کنار completeness شواهد و safety بسنجید. روند ترکیبی این معیارها از تعداد تست یا coverage گویاتر است.
جمعبندی
تستپذیری نرمافزار یعنی توان تبدیل یک ریسک واقعی به آزمایشی کنترلشده و شواهدی که بتوان بر اساس آن تصمیم گرفت. معماری ماژولار، DI، telemetry و Fake فقط ابزارند؛ ارزششان زمانی مشخص میشود که کنترل، مشاهده، Oracle، ایزولاسیون، بازتولید و تشخیص را برای یک سؤال مهم بهتر کنند.
از یک جریان درآمدی یا پرریسک شروع کنید، Contract کوتاه بنویسید، یک dependency را کنترلپذیر کنید، شواهد مستقل بسازید و Time to Evidence را اندازه بگیرید. بهبود کوچک اما سنجشپذیر، از بازطراحی وسیع با شعار «برای تست بهتر» قابلاعتمادتر است.

