یک تابع بازپرداخت فقط ۱۲ خط دارد؛ اما چهار تصمیم پیاپی در آن است. تیم شش Unit test نوشته و همه سبزند. آیا منطق کنترل کامل بررسی شده است؟ تعداد خط‌ها، تعداد Test caseها و حتی ۱۰۰٪ Statement coverage پاسخ کافی نمی‌دهند. باید ببینیم کنترل از چه مسیرهایی عبور می‌کند، کدام یال‌ها اجرا شده‌اند و آیا ترکیب تصمیم‌های مهم شواهد دارند.

تست مسیر (Path Testing) ساختار داخلی برنامه را با گراف جریان کنترل (CFG) مدل می‌کند و مسیرهای منتخب را به داده و Oracle تست تبدیل می‌کند. پیچیدگی سایکلوماتیک (Cyclomatic Complexity) یا V(G) ویژگی همین گراف و تعداد ابعاد مستقل فضای مسیرهاست. این عدد برای ساخت یک Basis set راهنماست؛ نه تعداد همه مسیرهای ممکن، نه حداقل جهانی Test case و نه نمره کیفیت نرم‌افزار.

خلاصه اجرایی:

  • All-path coverage در حضور Loop می‌تواند نامتناهی و در کد واقعی معمولاً غیرعملی باشد.
  • برای یک CFG متصل، V(G)=E−N+2؛ برای چند Component، V(G)=E−N+2P.
  • قاعده تعداد تصمیم+۱ فقط با تعریف سازگار از CFG و تصمیم‌های دودویی معتبر است؛ Switch، Short-circuit و Exception ممکن است شمارش ابزارها را متفاوت کنند.
  • Basis pathها مستقل خطی‌اند و انتخاب Basis یکتا نیست؛ پوشش آن‌ها مساوی پوشش همه مسیرها نیست.
  • V(G) را کنار اندازه تابع، تغییر، Coupling، داده تاریخی و ریسک محصول استفاده کنید؛ آستانه‌های ۱۰/۲۰/۵۰ قانون جهانی نیستند.

تست مسیر چیست؟

Path Testing یک تکنیک White-box است: تستر یا توسعه‌دهنده از ساختار کنترل کد خبر دارد، مسیرهای موردنظر را از CFG انتخاب می‌کند، ورودی مناسب برای پیمودن آن‌ها می‌سازد و نتیجه را با Oracle می‌سنجد. اگر به مبانی Statement، Branch و White-box نیاز دارید، راهنمای تست جعبه سفید و پوشش کد پیش‌نیاز مستقیم این مقاله است.

Path دقیقاً چیست؟

Path دنباله‌ای از Nodeها و Edgeهای CFG است که انتقال کنترل را نمایش می‌دهد. بسته به معیار، ممکن است از Entry به Exit، یک Simple path، Prime path یا مسیر دارای تکرار محدود Loop باشد. پس عبارت «Path coverage» بدون تعریف Coverage item مبهم است.

Basis Path Testing چیست؟

در Structured Testing، یک مجموعه Basis از مسیرها انتخاب می‌شود که هر مسیر تازه دست‌کم یک ترکیب/بعد مستقل نسبت به مسیرهای قبلی به فضای مسیر اضافه می‌کند. در نمایش برداری Edgeها، مسیرهای Basis مستقل خطی‌اند و مسیرهای دیگر می‌توانند از ترکیب خطی آن‌ها ساخته شوند. تعداد اعضای یک Basis کامل با Cyclomatic complexity گراف مرتبط است؛ خود مجموعه Basis یکتا نیست.

راهنمای اصلی NIST SP 500-235: Structured Testing معیار را اجرای یک Basis set از مسیرهای CFG هر Module تعریف می‌کند و آن را مکمل تست مبتنی بر نیازمندی می‌داند، نه جایگزین آن.

Path Testing چه چیزی نیست؟

  • تضمین اجرای همه مسیرهای ممکن نیست؛
  • صرفاً مشاهده درصد Line coverage نیست؛
  • جایگزین تست نیازمندی، Boundary، State، Security یا Performance نیست؛
  • اثبات نبود Defect پس از پوشش Basis نیست؛
  • الگوریتم خودکار تولید ورودی برای هر مسیر نیست؛ Constraint solving مسئله جداگانه‌ای است.

پیچیدگی سایکلوماتیک چیست؟

Thomas J. McCabe این معیار را در مقالهٔ A Complexity Measure (1976) معرفی کرد. Cyclomatic complexity از توپولوژی گراف کنترل می‌آید و به‌طور شهودی تعداد تصمیم‌های مستقلی را نشان می‌دهد که فضای مسیر را گسترش داده‌اند.

V(G) چه اطلاعاتی می‌دهد؟

  • اندازه Basis فضای مسیرهای CFG؛
  • سیگنال تراکم تصمیم در یک Function/Method؛
  • راهنما برای اینکه Branch/Path evidence بیشتری احتمالاً لازم است؛
  • مبنای مقایسه یک Method با نسخه قبلی خودش، با تعریف ابزار ثابت.

V(G) چه اطلاعاتی نمی‌دهد؟

  • درست‌بودن Requirement یا Business rule؛
  • خوانایی نام‌ها، Cohesion، Coupling یا پیچیدگی شناختی کامل؛
  • تعداد Defectهای موجود یا احتمال قطعی Failure؛
  • زمان دقیق طراحی/اجرای تست؛
  • تعداد همه Entry-to-exit pathها؛
  • کیفیت Oracle و داده تست؛
  • ریسک کسب‌وکار بدون Context.

یک Function با V(G)=۲ می‌تواند انتقال وجه اشتباه انجام دهد و بسیار پرریسک باشد؛ Function با V(G)=۱۵ ممکن است Parser کم‌اثر و خوب‌آزموده‌ای باشد. برای تصمیم اولویت، Complexity را با مدل تست مبتنی بر ریسک ترکیب کنید.

گراف جریان کنترل یا CFG را چگونه بسازیم؟

Node یا Basic block

یک Basic block دنباله‌ای خطی از دستورهاست که ورود به ابتدای آن و خروج از انتهای آن رخ می‌دهد؛ در میانه Branch نمی‌زنیم. ابزارها ممکن است Source line، Bytecode instruction یا IR block را مبنا بگیرند، بنابراین Node count میان ابزارها الزاماً یکسان نیست.

Edge

Edge انتقال کنترل میان Nodeهاست: ادامه مستقیم، نتیجه True/False، Case در Switch، Back edge حلقه، Return یا Exception. برخی ابزارهای Coverage، Exception edge را در Branch count وارد نمی‌کنند؛ باید Definition همان ابزار را بخوانید.

Decision و Predicate

Decision نقطه‌ای است که چند Outcome کنترل دارد. در یک if ساده معمولاً دو Outcome داریم. در switch تعداد خروجی‌ها بیشتر است. در عبارت a && b، Short-circuit می‌تواند دو Condition و چند انتقال کنترل بسازد، حتی اگر در Source یک if دیده شود.

Entry، Exit و چند خروجی

برای محاسبه دستی شفاف، یک Entry و یک Exit مفهومی رسم کنید و همه Returnها/Throwهای در Scope را به Exit متصل کنید. این کار مسیرهای Early return را قابل‌شمارش می‌کند. مدل Compiler ممکن است Exit یا Exception را متفاوت نمایش دهد؛ عدد را فقط در یک Convention مقایسه کنید.

فرمول‌های محاسبه V(G)

فرمول گرافی عمومی

V(G) = E − N + 2P

  • E: تعداد Edgeها؛
  • N: تعداد Nodeها؛
  • P: تعداد Componentهای متصل در Scope تحلیل؛
  • برای یک Function متصل، معمولاً P=1 و V(G)=E−N+2.

فرمول Decision + ۱

برای CFG ساخت‌یافته‌ای که همه Decisionها دودویی مدل شده‌اند:

V(G) = D + 1

این میان‌بُر را کورکورانه به Source code تعمیم ندهید. اگر یک Decision دارای k خروجی باشد، سهم آن در بیان عمومی‌تر k−1 است. ابزارهایی مانند JaCoCo از شمار Branchها و Decision points استفاده می‌کنند:

V(G) = B − D + 1

مستندات رسمی JaCoCo Coverage Counters همین تعریف را بیان می‌کند و می‌گوید Exception handling را در Branch counter خود حساب نمی‌کند. پس عدد JaCoCo را با عدد ابزاری که Exception edge را می‌شمارد بی‌توضیح مقایسه نکنید.

چرا عدد ابزارها فرق می‌کند؟

  • Source، AST، Bytecode یا IR متفاوت؛
  • Short-circuit Booleanها به‌عنوان یک یا چند Decision؛
  • Switch با Default ضمنی یا بدون آن؛
  • Catch/Finally/Exception edge؛
  • Compiler-generated code، Coroutine و State machine؛
  • Macro، Template، Inline و Optimization؛
  • تعریف Scope در سطح Method، Class یا File.

در Dashboard همیشه نام/نسخه ابزار، زبان، Scope و Counter definition را کنار عدد نگه دارید. تغییر ابزار می‌تواند Trend جعلی بسازد.

مثال کامل: تصمیم بازپرداخت فروشگاه

تابع زیر پنج خروجی کنترلی ممکن را از چهار Decision دودویی می‌سازد. برای سادگی، Validation نوع و جزئیات مالی خارج از Scope مثال‌اند:

type RefundAction =
  | "invalid"
  | "reject"
  | "manual-review"
  | "instant"
  | "queue";

function decideRefund(
  amount: number,
  refundableBalance: number,
  customerVerified: boolean,
  gatewayAvailable: boolean
): RefundAction {
  if (amount <= 0) {
    return "invalid";
  }

  if (amount > refundableBalance) {
    return "reject";
  }

  if (!customerVerified) {
    return "manual-review";
  }

  if (gatewayAvailable) {
    return "instant";
  }

  return "queue";
}

CFG متنی و شماره Nodeها

1 Entry
  ↓
2 amount <= 0 ?
  T → 3 return invalid → 11 Exit
  F ↓
4 amount > refundableBalance ?
  T → 5 return reject → 11 Exit
  F ↓
6 !customerVerified ?
  T → 7 return manual-review → 11 Exit
  F ↓
8 gatewayAvailable ?
  T → 9 return instant → 11 Exit
  F → 10 return queue → 11 Exit

شمارش Node و Edge

  • Nodeها: N=11؛
  • Edgeها: Entry→۲، دو خروجی Node ۲، ۳→Exit، دو خروجی ۴، ۵→Exit، دو خروجی ۶، ۷→Exit، دو خروجی ۸، ۹→Exit و ۱۰→Exit؛ در مجموع E=14؛
  • Component: P=1.

V(G)=14−11+2×1=5

چهار Decision دودویی هم داریم، پس کنترل دوم: D+1=4+1=5. توافق دو محاسبه خطای رسم/شمارش را کمتر می‌کند، هرچند اثبات کامل درست‌بودن CFG نیست.

پنج مسیر Entry-to-exit در این مثال

  1. 1-2T-3-11؛
  2. 1-2F-4T-5-11؛
  3. 1-2F-4F-6T-7-11؛
  4. 1-2F-4F-6F-8T-9-11؛
  5. 1-2F-4F-6F-8F-10-11.

به‌علت زنجیره Early return و نبود Loop، این Function کوچک دقیقاً همین پنج مسیر کامل feasible را دارد. این هم‌زمانی با V(G)=5 ویژگی این ساختار ساده است؛ در Functionهای دارای Loop یا مسیرهای ترکیبی، تعداد مسیرهای Entry-to-exit می‌تواند بسیار بیشتر از V(G) باشد.

داده تست و Oracle مستقل

Path amount balance verified gateway Expected
P1 −۱ ۱۰۰ true true invalid
P2 ۱۱۰ ۱۰۰ true true reject
P3 ۵۰ ۱۰۰ false true manual-review
P4 ۵۰ ۱۰۰ true true instant
P5 ۵۰ ۱۰۰ true false queue

این پنج تست مسیرها را می‌پیمایند، ولی Boundaryهای 0، balance و balance+1 را کامل بررسی نمی‌کنند. برای داده مرزی از تقسیم‌بندی هم‌ارزی و تحلیل مقدار مرزی استفاده کنید. Path coverage و ورودی‌محور بودن دو زاویه مکمل‌اند.

Unit test نمونه

describe("decideRefund", () => {
  test.each([
    [-1, 100, true, true, "invalid"],
    [110, 100, true, true, "reject"],
    [50, 100, false, true, "manual-review"],
    [50, 100, true, true, "instant"],
    [50, 100, true, false, "queue"],
  ] as const)(
    "returns %s/%s/%s/%s → %s",
    (amount, balance, verified, gateway, expected) => {
      expect(
        decideRefund(amount, balance, verified, gateway)
      ).toBe(expected);
    }
  );
});

نام تست و Failure output باید مسیر کسب‌وکاری را هم قابل‌فهم کند؛ Test table تنها وقتی ارزش دارد که شکست یک Row را دقیق نشان دهد. اگر Refactor خروجی را عوض نکرد اما CFG را تغییر داد، تست رفتار و Coverage report هر دو باید بازبینی شوند.

Basis Path را گام‌به‌گام استخراج کنیم

گام ۱: Scope و Convention را ثابت کنید

Method، Entry/Exit، Exception، Compound condition و Generated code را تعریف کنید. بدون این قرارداد، اختلاف V(G) الزاماً خطا نیست.

گام ۲: CFG را بسازید و Complexity را دوبار حساب کنید

از E−N+2P و در صورت معتبر بودن از Decision-based formula استفاده کنید. اگر دو نتیجه متفاوت است، Edge/Node، Switch، Short-circuit یا Exit را دوباره بررسی کنید.

گام ۳: یک مسیر پایه انتخاب کنید

یک Entry-to-exit path ساده و feasible بردارید. در مثال، P5 می‌تواند مسیر پایه باشد.

گام ۴: هر بار Edge/بعد تازه اضافه کنید

مسیرهای بعدی را طوری انتخاب کنید که نسبت به مجموعه موجود مستقل باشند. ترتیب P5، P4، P3، P2، P1 یک Basis ممکن است؛ ترتیب دیگری نیز می‌تواند معتبر باشد.

گام ۵: Constraint مسیر را حل کنید

برای هر مسیر، شرط‌های طی‌شده را به Path condition تبدیل کنید. مثال P4:

  • amount > 0؛
  • amount <= refundableBalance؛
  • customerVerified = true؛
  • gatewayAvailable = true.

سپس ورودی‌ای بسازید که همه قیدها را هم‌زمان ارضا کند. یک ورودی که فقط آخرین Branch را True کند کافی نیست.

گام ۶: Oracle و Side effect را تعریف کنید

رسیدن به Path بدون تشخیص نتیجه غلط، Test کامل نیست. Return value، State change، Event، DB write، Call count، Log و خطای ممنوع را بسته به Function بسنجید.

گام ۷: با Instrumentation تأیید کنید

انتظار دستی را با Coverage report مقایسه کنید. اگر Branch اجرا نشده، داده یا مدل اشتباه است. اگر ابزار Branch اضافی نشان داد، احتمالاً Short-circuit، Compiler code یا Exception semantics را ندیده‌اید.

گام ۸: Basis و Evidence را نسخه‌دار کنید

Path ID، Source revision، Tool version، Input، Expected، Observed و Covered edges را نگه دارید. با تغییر CFG، Basis قبلی می‌تواند نامعتبر شود؛ Test caseهای رفتاری شاید بمانند اما ادعای Coverage باید دوباره محاسبه شود.

تفاوت Path، Branch، Condition و MC/DC Coverage

معیار Coverage item قدرت محدودیت اصلی
Statement دستور قابل‌اجرا نشان می‌دهد کد اجرا شده Outcome دیگر Decision را ممکن است نبیند
Branch/Decision انتقال/Outcome کنترل True/False و Caseها را می‌سنجد ترکیب چند Decision در یک Path را الزام نمی‌کند
Condition True/False هر شرط اتمی Conditionهای عبارت مرکب را می‌بیند اثر مستقل شرط بر Decision را الزاماً نشان نمی‌دهد
MC/DC اثر مستقل هر Condition بر نتیجه Decision برای منطق Boolean حساس دقیق‌تر است هزینه و Instrumentation بالاتر؛ همه Pathهای برنامه نیست
Basis Path مجموعه مستقل خطی مسیرهای CFG تعامل ساختاری فراتر از Branch منفرد همه مسیرها یا همه داده‌ها را پوشش نمی‌دهد
Prime Path Simple path بیشینه که زیرمسیر دیگری نیست Loop و تعامل مسیر را نظام‌مندتر می‌بیند Path explosion و Infeasible path
All Complete Paths همه مسیرهای Entry-to-exit در CFG محدود بدون Loop قوی است با Loop می‌تواند نامتناهی باشد

ISTQB CTFL 4.0.1 می‌گوید ۱۰۰٪ Branch coverage، Statement coverage را subsume می‌کند؛ اما حتی Branch coverage کامل ممکن است Defect وابسته به یک Path خاص را نبیند. رابطه Coverageها را فقط با Definition دقیق همان معیار بیان کنید.

مثال Compound condition

if (customerVerified && amount <= instantLimit) {
  return "instant";
}

در Source یک if دیده می‌شود، ولی دو Condition اتمی داریم. به‌علت Short-circuit، وقتی customerVerified=false است شرط دوم ارزیابی نمی‌شود. یک ابزار ممکن است Complexity را بر Branchهای تولیدشده حساب کند، دیگری بر Decision source-level. اگر ریسک به اثر مستقل هر Condition وابسته است، Branch coverage ساده را با Condition/MC/DC تکمیل کنید.

مستندات Clang Source-based Coverage Line، Region، Branch و MC/DC را جدا تعریف می‌کند و گزارش می‌دهد. همین جداسازی نشان می‌دهد «Coverage=۱۰۰%» بدون نام Counter معنا ندارد.

Loop، Path Explosion و معیار عملی

چرا تعداد مسیرها منفجر می‌شود؟

یک Loop می‌تواند صفر، یک، دو یا تعداد زیادی بار اجرا شود؛ دو Loop یا شرط تودرتو ترکیب‌ها را چندبرابر می‌کنند. اگر Iteration محدود نباشد، مجموعه Complete pathها نامتناهی است. V(G) با تعداد Decisionها رشد می‌کند، اما تعداد مسیرهای اجرایی ممکن است بسیار سریع‌تر رشد کند.

استراتژی تست Loop

  • صفر Iteration، اگر از نظر Requirement و کنترل feasible است؛
  • یک Iteration برای ورود/خروج پایه؛
  • دو Iteration برای آشکارکردن وابستگی بین تکرارها؛
  • Boundaryهای واقعی مانند max−1، max و رفتار max+1؛
  • Termination، Progress invariant و Side effect در هر Iteration؛
  • داده تکراری، ترتیب متفاوت، خطا در میانه و Resume/Retry؛
  • ترکیب منتخب Loop با Decisionهای پرریسک، نه ضرب کور همه حالت‌ها.

Prime Path Coverage

Prime path، Simple path بیشینه‌ای است که زیرمسیر Simple دیگری نیست؛ Nodeها در Simple path تکرار نمی‌شوند جز اینکه ابتدا و انتها برای Cycle یکی باشند. نسخه‌های جاری GCC می‌توانند با -fpath-coverage Prime pathها را Instrument کنند و خود مستندات درباره رشد سریع مسیرها و Limit پیش‌فرض محافظتی هشدار می‌دهد؛ نگاه کنید به GCC Instrumentation Options.

Prime path هم همه مقدارهای داده یا تعداد Iteration را پوشش نمی‌دهد. آن را برای Functionهای منتخب و مهم استفاده کنید، نه Gate سراسری بی‌قید.

مسیر Infeasible چیست و چه کنیم؟

مسیر Infeasible در CFG از نظر Edgeها قابل‌رسم است اما هیچ ورودی/State مجازی همه قیودش را هم‌زمان ارضا نمی‌کند.

if (x > 0) {
  audit("positive");
}

if (x <= 0) {
  return "non-positive";
}

return "positive";

مسیر «تصمیم اول True و تصمیم دوم True» به x>0 و x<=0 هم‌زمان نیاز دارد و Infeasible است. هر Branch به‌تنهایی feasible است، اما آن ترکیب خاص نه.

گردش‌کار برخورد با مسیر غیرقابل‌اجرا

  1. Path condition را صریح بنویسید؛
  2. وابستگی داده، State، Type، Overflow و Environment را بررسی کنید؛
  3. با Review یا Solver نتیجه را تأیید کنید؛
  4. Path ID، دلیل، Source revision و تأییدکننده را ثبت کنید؛
  5. اگر Constraint ناشی از Coupling غیرضروری است، Refactor/Testability را بررسی کنید؛
  6. Coverage denominator را فقط طبق Policy و با Audit trail تغییر دهید.

NIST SP ۵۰۰-۲۳۵ امکان تفاوت «Actual complexity» با Cyclomatic complexity را وقتی Basis کامل قابل‌اجرا نیست مطرح می‌کند. Label کردن مسیر به‌عنوان Infeasible نباید راه میان‌بُر سبزکردن گزارش باشد؛ با تغییر کد، استدلال باید دوباره بازبینی شود.

Data Flow و Path Testing چه ارتباطی دارند؟

CFG فقط ترتیب انتقال کنترل را نشان می‌دهد. یک Path ممکن است پیموده شود اما Definition یک متغیر به Use موردنظر نرسد، یا Alias و State اثر دیگری ایجاد کند. برای Def-use pair، def-clear path و معیارهای All-Defs/All-Uses/All-DU-Paths به راهنمای تست جریان داده و Def-Use مراجعه کنید.

Path condition با Data state متفاوت است

Path condition قید ورود به مسیر است؛ Oracle درباره نتیجه و State نهایی داوری می‌کند. در مثال بازپرداخت، شرط‌ها ما را به instant می‌رسانند، اما صحت مبلغ، ثبت Ledger و عدم دوباره‌پرداخت به Data-flow، State و Side-effect test نیاز دارد.

State machine و Path

CFG مسیر داخل یک اجرا/Module را مدل می‌کند؛ State transition sequence می‌تواند چند درخواست، Event یا Session را دربرگیرد. برای Refund که از Requested→Approved→Sent→Settled می‌رود، تست انتقال حالت مکمل بهتری برای توالی کسب‌وکاری است.

Path Testing را با تکنیک‌های Black-box ترکیب کنید

Decision Table برای Ruleهای کسب‌وکار

CFG می‌گوید چه Control pathهایی در پیاده‌سازی وجود دارد؛ Decision table می‌گوید ترکیب Ruleهای نیازمندی چه Actionی باید بسازد. اگر Developer یک Rule را اصلاً پیاده نکرده باشد، White-box coverage ممکن است سبز شود و Omission را نبیند. راهنمای تست جدول تصمیم این شکاف را پوشش می‌دهد.

Boundary و Equivalence

یک Path می‌تواند با یک ورودی داخلی Partition پوشش داده شود ولی Defect مرزی باقی بماند. برای شرط amount>balance دست‌کم balance−1، balance و balance+1 را بر اساس Domain بررسی کنید.

Risk-based selection

اگر ده‌ها Function پیچیده دارید، فقط V(G) را Descending sort نکنید. Exposure، Criticality، Change frequency، Ownership، Incident history و Testability را اضافه کنید. Function ساده پولی ممکن است از Parser پیچیده داخلی اولویت بالاتری داشته باشد.

Complexity را چگونه برای مهندسی کد استفاده کنیم؟

Trend در سطح Method، نه میانگین فریبنده

میانگین File می‌تواند یک Method با V(G)=۴۰ را میان ده Method ساده پنهان کند. Distribution، Maximum، Changed-method complexity و Missed complexity را ببینید. Compare فقط با نسخه و Definition ثابت معنی دارد.

آستانه را Local و Review-based کنید

بازه‌های «۱–۱۰ خوب، ۱۱–۲۰ متوسط، بالای ۵۰ بد» Context، زبان و اندازه را حذف می‌کنند. Policy بهتر:

  • افزایش Complexity در کد تغییرکرده نیازمند توضیح و Test evidence باشد؛
  • Methodهای Outlier وارد Review شوند، نه اینکه خودکار «معیوب» نامیده شوند؛
  • Gate با Baseline و Exception دارای Owner/Expiry کار کند؛
  • آستانه از داده Repository و ظرفیت تیم کالیبره شود.

Refactor همیشه فقط Extract Method نیست

  • Guard clause برای کاهش Nesting—بدون ادعای قطعی کاهش V(G)؛
  • تقسیم مسئولیت و استخراج Policy/Strategy؛
  • تبدیل شرط‌های پراکنده به State/Decision model؛
  • حذف Branch غیرممکن یا Duplicate؛
  • تفکیک I/O از منطق خالص برای Testability؛
  • ساخت نام دامنه‌ای برای Predicate پیچیده؛
  • حفظ Characterization test پیش از تغییر.

Extract Method ممکن است Complexity هر Method را پایین بیاورد اما Complexity کل رفتار و Coupling را جابه‌جا کند. Design را با Cohesion، Coupling، خوانایی و هزینه تغییر هم بسنجید.

Complexity و Static Analysis

Metric معمولاً بدون اجرای برنامه محاسبه می‌شود و در خانواده تحلیل استاتیک قرار می‌گیرد؛ اما Coverage از اجرای Instrumented test می‌آید. برای سیاست Rule، Baseline، Triage و Quality Gate از راهنمای تحلیل استاتیک کد برای QA استفاده کنید.

ابزارها و پیاده‌سازی در CI

سه دسته ابزار

  • Static metric/CFG: Complexity و ساختار را بدون اجرا محاسبه می‌کند؛
  • Coverage instrumentation: Edge/Branch/Region اجراشده را از Test run ثبت می‌کند؛
  • Path/constraint tooling: Prime path، Symbolic execution یا Test generation را با محدودیت زبان/Scope انجام می‌دهد.

JaCoCo برای JVM Branch و Complexity covered/missed گزارش می‌کند؛ GCC/gcov Prime-path coverage دارد؛ Clang/LLVM Source-based coverage، Branch و MC/DC را جدا می‌سنجد. نام ابزار به‌تنهایی کافی نیست—نسخه، Compiler flag، Optimization، Source mapping و Definition counter را ثبت کنید.

Pipeline پیشنهادی

structural-evidence:
  inputs:
    source_revision: exact_commit
    toolchain: pinned_version
    test_binary: coverage_instrumented
  steps:
    - run_unit_and_component_tests
    - merge_coverage_from_all_shards
    - verify_no_test_or_binary_is_missing
    - report_statement_branch_condition_metrics
    - report_changed_method_cyclomatic_complexity
    - inspect_selected_high_risk_paths
    - validate_documented_infeasible_paths
  artifacts:
    - raw_coverage
    - normalized_report
    - tool_version
    - source_map
    - path_evidence
  gate:
    - fail_on_incomplete_instrumentation
    - review_complexity_regression_in_changed_code
    - require_risk_specific_coverage_targets

پیش از اعتماد به درصد Coverage

  • همه Test shardها Merge شده‌اند؟
  • همان Binary و Commit گزارش شده است؟
  • کد بدون Instrumentation وارد Build نشده است؟
  • Generated/vendor/test code در Denominator چگونه است؟
  • Process crash یا Fork داده Coverage را از بین نبرده است؟
  • Exception/async/coroutine با ابزار چگونه مدل می‌شود؟
  • Exclusion جدید Review و دلیل دارد؟

Gate خوب چه می‌سنجد؟

یک Threshold جهانی Complexity یا Coverage به‌تنهایی کافی نیست. Gate را بر Diff و ریسک بنا کنید: Instrumentation سالم، Branchهای تغییرکرده پرریسک پوشش‌داده‌شده، Regression غیرموجه Complexity بررسی‌شده و Exclusionها ممیزی‌پذیر باشند. Trend و Definition متریک در راهنمای متریک‌های تست تکمیل شده است.

محدودیت‌ها و دام‌های رایج

Basis path را با All paths یکی می‌گیریم

V(G)=۸ یعنی Basis هشت‌بعدی، نه اینکه Function فقط هشت مسیر دارد. Loop و ترکیب Decisionها می‌تواند مسیرهای بسیار بیشتری بسازد.

تعداد Test case را از V(G) تخمین قطعی می‌زنیم

یک Test ممکن است چند Function/path را در اجرای Integration بپیماید؛ یک Path ممکن است برای Boundary، Data state و Oracleهای مختلف چند Test بخواهد؛ مسیر Infeasible ممکن است داده نداشته باشد. V(G) فقط indication ساختاری است.

Coverage را با کیفیت یکی می‌گیریم

Coverage می‌گوید چه ساختاری اجرا شده، نه اینکه Assertion درست، Requirement کامل، داده نماینده یا Failure مهم کشف شده است. Test بدون Oracle می‌تواند Coverage را بالا ببرد و چیزی را بررسی نکند.

Complexity بالا را علت Defect قطعی می‌دانیم

همبستگی‌های تاریخی ممکن است با LOC، Change churn، Ownership و Domain مخدوش شوند. Metric را Trigger بررسی و داده محلی قرار دهید، نه قضاوت فردی یا KPI توسعه‌دهنده.

مسیرهای Exception را فراموش می‌کنیم

Throw، Timeout، Cancellation و Resource cleanup ممکن است در Branch counter ابزار نیایند. Failure-path test را از Requirement و Risk طراحی کنید، نه فقط رنگ Coverage.

Short-circuit را یک شرط می‌بینیم

عبارت مرکب چند Condition دارد و بعضی Operandها اجرا نمی‌شوند. Source line سبز یا Decision True/False ممکن است اثر مستقل هر شرط را ثابت نکند.

Refactor را فقط برای سبزکردن Gate انجام می‌دهیم

تقسیم مکانیکی Function می‌تواند Navigation و Coupling را بدتر کند یا Branchها را جابه‌جا کند. Outcome، تست رفتار، Design review و Complexity distribution را با هم ببینید.

ملاحظات تیم‌های ایرانی

Money و رقم فارسی

CFG فقط Branch مبلغ را نشان می‌دهد؛ واحد ریال/تومان، Overflow، Decimal، Rounding و رقم فارسی به Partition/Boundary/Property test نیاز دارد. Oracle مبلغ را مستقل از همان Function تحت تست بسازید.

تقویم، Timezone و Cut-off

شرط‌های تاریخ ممکن است در روز عادی V(G) کمی داشته باشند اما در تغییر روز، ماه، سال کبیسه یا Cut-off بانکی اثر بزرگی بسازند. Clock قابل‌کنترل و داده شمسی/میلادی مرزی لازم است.

Dependency و قطعی

Branchهای Timeout، Retry، Circuit state و Fallback را صریح مدل کنید. Sandbox درگاه یا پیامک ممکن است Exception/Callback واقعی را تولید نکند؛ Stub کنترل‌شده، Contract و آزمایش محدود محیطی را ترکیب کنید.

CI و Runner محدود

Coverage artifact را داخل Runner امن نگه دارید، Shardها را deterministic Merge کنید و Compiler/toolchain را Cache و نسخه‌دار کنید. اگر ابزار Cloud کد یا گزارش مسیر را خارج می‌فرستد، محرمانگی Source و قرارداد را بررسی کنید.

برنامه عملی چهارمرحله‌ای

مرحله ۱: انتخاب Scope

  • سه تا پنج Function پرریسک یا پرChange انتخاب کنید.
  • Tool/CFG convention و Counter definition را ثبت کنید.
  • Statement/Branch baseline و Complexity فعلی را ذخیره کنید.

مرحله ۲: تحلیل دستی یک نمونه

  • CFG را با Entry/Exit رسم کنید.
  • V(G) را با دو فرمول کنترل کنید.
  • Basis path، Path condition، ورودی و Oracle بسازید.

مرحله ۳: Instrumentation و اختلاف

  • Testها را روی Binary دقیق اجرا کنید.
  • Branchهای Missed و عدد ابزار را با مدل دستی مقایسه کنید.
  • Short-circuit، Exception و Infeasible path را مستند کنید.

مرحله ۴: Policy تدریجی

  • ابتدا Report-only روی Changed methods؛
  • Review برای Regression و Outlier، نه Fail سراسری؛
  • Coverage target متناسب با ریسک و Counter مشخص؛
  • Retrospective ماهانه درباره Defectهای ازدست‌رفته و Metric gaming.

چک‌لیست Path Testing

  • Scope، Source revision و Tool version ثابت است.
  • Entry، Exit، Exception و Compound condition convention تعریف شده‌اند.
  • Node و Edgeها مستقل شمارش و V(G) کنترل شده است.
  • Basis path با All path اشتباه نشده است.
  • برای هر Path، Path condition و ورودی feasible داریم.
  • Oracle فقط «عدم Crash» نیست و Side effect را می‌سنجد.
  • Boundary، Decision table، State و Data-flow مکمل انتخاب شده‌اند.
  • Loop با Bound/Invariant و معیار مسیر مشخص پوشش داده شده است.
  • Infeasible path دلیل، Revision و تأیید دارد.
  • Coverage artifact کامل و مربوط به همان Commit/Binary است.
  • Complexity به‌تنهایی KPI فردی، Quality score یا Effort estimate نیست.
  • Gate روی Risk، Diff و سلامت Instrumentation است.

سوالات متداول تست مسیر و Cyclomatic Complexity

آیا V(G) برابر تعداد Test caseهای لازم است؟

نه به‌صورت عمومی. V(G) اندازه یک Basis از مسیرهای مستقل CFG است و می‌تواند راهنمای تعداد Pathهای Basis باشد. یک Path ممکن است چند داده/Oracle بخواهد، یک Test چند مسیر را بپیماید و بعضی Pathها Infeasible باشند. بنابراین از V(G) به‌عنوان عدد قطعی Test case یا زمان استفاده نکنید.

آیا Basis Path Coverage همان All-path Coverage است؟

خیر. Basis مجموعه‌ای از مسیرهای مستقل خطی با اندازه مرتبط با V(G) است. All-path همه مسیرهای تعریف‌شده را می‌خواهد و با Loop می‌تواند نامتناهی باشد. در مثال Early-return این مقاله هر دو اتفاقاً پنج مسیر دارند، اما این یک ویژگی خاص مثال است.

فرمول D+۱ همیشه درست است؟

فقط وقتی CFG و Decisionهای دودویی با Convention آن فرمول سازگار باشند. Switch چندخروجی، شرط Short-circuit، Exception و مدل Bytecode می‌توانند شمارش را عوض کنند. فرمول گرافی E−N+2P مرجع دقیق‌تری است و باید Definition ابزار را هم خواند.

تفاوت Branch Coverage و Path Coverage چیست؟

Branch coverage می‌سنجد هر انتقال/Outcome حداقل یک بار اجرا شده است؛ Path coverage ترکیب انتقال‌ها در دنباله‌های مشخص را می‌سنجد. ممکن است همه Branchها جداگانه اجرا شوند اما ترکیب خاص دو Decision هرگز آزموده نشود. در مقابل، هیچ Coverage ساختاری جای Requirement و Oracle را نمی‌گیرد.

چه عددی برای Cyclomatic Complexity خوب است؟

آستانه جهانی قابل‌دفاعی برای همه زبان‌ها و Domainها وجود ندارد. Method کوچک و پایدار را با Method تغییرکرده و مالی یکسان قضاوت نکنید. Distribution Repository، Trend همان Method، Risk، LOC، Change churn، Coupling و کیفیت تست را ببینید؛ از عدد به‌عنوان Trigger Review استفاده کنید.

جمع‌بندی: CFG را به شواهد تبدیل کنید

ارزش تست مسیر در رسم گراف نیست؛ در تبدیل Edge و Path به Constraint، داده، Oracle و Evidence است. Cyclomatic complexity به ما می‌گوید فضای کنترل چند بعد مستقل دارد، اما همه مسیرها، همه ورودی‌ها یا ریسک محصول را خلاصه نمی‌کند. یک اجرای حرفه‌ای، Basis را از All paths جدا می‌کند، مسیر Infeasible و Loop را صریح مدیریت می‌کند، Definition ابزار را نسخه‌دار نگه می‌دارد و Coverage ساختاری را با تکنیک‌های نیازمندی‌محور تکمیل می‌کند.

روش تدوین و منابع

این مقاله با بازسازی مستقل CFG مثال، شمارش Node/Edge، کنترل دو فرمول و تطبیق تعریف‌ها با منابع اصلی و مستندات ابزارهای جاری تدوین شده است. Syntax و Counter ابزارها با نسخه تغییر می‌کند؛ مستندات همان Toolchain پروژه را مبنا قرار دهید.

  • McCabe (1976), A Complexity Measure, IEEE TSE
  • NIST SP 500-235, Structured Testing
  • ISTQB CTFL ۴.۰.۱, Statement و Branch coverage
  • JaCoCo Coverage Counters, Branch و Cyclomatic complexity
  • GCC/gcov, Prime-path instrumentation and reporting
  • Clang/LLVM Source-based Coverage, Branch و MC/DC

بازبینی محتوایی: مرداد ۱۴۰۵. برای نرم‌افزارهای Safety-critical یا تحت استاندارد، معیار Coverage و استقلال تست مصوب همان دامنه بر مثال عمومی این متن مقدم است.

دیدگاهتان را بنویسید