সরাসরি পর্বে যান
পর্ব ৫০ · ৭০-এর মধ্যে · মডিউল ৮

Coverage আর complexity — statement, branch coverage, cyclomatic complexity আর mutation testing

Coverage and Complexity — Statement and Branch Coverage, Cyclomatic Complexity and Mutation Testing

Taught in Bangla, this lesson draws control-flow graphs and defines statement, branch, condition, MC/DC, multiple-condition and path coverage with their subsumption order. It computes cyclomatic complexity three ways, derives basis path tests, contrasts cognitive complexity, explains coverage tools and target traps, and uses mutation testing to measure real test strength, with hands-on simulators.

এই পর্বে যা শিখবেন

  • যেকোনো ছোট function থেকে control-flow graph আঁকা — node, edge, decision node, entry আর exit
  • Statement, branch, condition, condition/decision, MC/DC, multiple-condition, path আর basis-path coverage-এর নিখুঁত সংজ্ঞা, আর কোনটি কোনটিকে subsume করে
  • Cyclomatic complexity তিনভাবে হিসাব করে মেলানো, আর basis path testing দিয়ে test case বের করা
  • Cognitive complexity কেন আলাদা, আর complexity-র সীমা কতটা নিয়ম, কতটা রীতি
  • Coverage tool-এর branch mode, coverage target-এর ফাঁদ, আর mutation testing দিয়ে test-এর আসল শক্তি মাপা

১ · Coverage কী মাপে — আর কী মাপে না

পর্ব ৪৫-এ (Testing-এর মৌলিক) দেখেছেন, white-box testing-এ test নকশা হয় কোডের ভেতরের গঠন দেখে, আর black-box-এ শুধু বাইরের আচরণ দেখে। পর্ব ৪৭-এ (Test case নকশা) black-box কৌশল — equivalence partitioning, boundary value আর decision table. এই পর্ব white-box দিকের মাপকাঠি: আপনার test কোডের কোন অংশ ছুঁয়েছে, কোন অংশ ছোঁয়নি, আর কোডটি নিজে কতটা জটিল।

একজন food-delivery rider-এর কথা ভাবুন, যাঁকে ধানমন্ডির সব রাস্তা চেনানো হচ্ছে। মালিক তাঁর GPS-এর রেকর্ড দেখে বললেন, “আপনি ৯০% রাস্তায় গেছেন।” ভালো খবর — কিন্তু তাতে বোঝা যায় না তিনি প্রতিটি বাড়ির নম্বর মিলিয়ে দেখেছেন কি না। আবার বাকি ১০% রাস্তার খবর একেবারে নিশ্চিত: সেখানে তিনি যাননি। Test coverage-ও এমন। কোন লাইন বা শাখা চলেনি, সেটি নিশ্চিত তথ্য। কিন্তু যা চলেছে, তা ঠিকভাবে যাচাই হয়েছে কি না — coverage তা বলে না।

Coverage-কে বলা হয় test adequacy criterion — test suite “যথেষ্ট” কি না তার একটি মাপকাঠি। প্রতিটি মাপকাঠি একটি প্রশ্ন: “প্রতিটি লাইন অন্তত একবার চলেছে?” “প্রতিটি if-এর দুই দিকই নেওয়া হয়েছে?” “প্রতিটি শর্ত আলাদাভাবে ফলাফল বদলাতে পেরেছে?” প্রশ্ন যত কড়া, পূরণ করতে test তত বেশি লাগে। Coverage-এর সাধারণ সূত্র:

Coverage (%) = যতগুলো লক্ষ্য অন্তত একবার ছোঁয়া হয়েছে ÷ মোট লক্ষ্য × ১০০
লক্ষ্য = statement, branch, condition, path… মাপকাঠি অনুযায়ী
মূল ধারণা

Coverage প্রয়োজনীয় কিন্তু যথেষ্ট নয়। যে লাইন কোনো test-এ চলেনি, সেখানে bug থাকলে কোনো test তা ধরবে না — এটি নিশ্চিত। কিন্তু লাইন চললেই bug ধরা পড়ে না: test-কে ফলও যাচাই করতে হয় (assertion), আর সঠিক ইনপুট দিয়ে চালাতে হয় (যেমন সীমার মান)। তাই এই পর্বের শেষ অংশে আসবে mutation testing — test আসলে ভুল ধরে কি না, তার সরাসরি মাপ।

২ · Control-flow graph — কোড থেকে ছবি

সব white-box মাপের ভিত্তি একটি ছবি: control-flow graph (CFG). রেলের রুট-ম্যাপ যেমন দেখায় কোন স্টেশন থেকে কোন স্টেশনে যাওয়া যায়, CFG তেমনি দেখায় কোডের কোন অংশের পরে কোন অংশ চলতে পারে। পর্ব ২৩ (Requirement modeling) আর পর্ব ৩০-এর (UML behaviour diagram) activity diagram-এ পথ গোনা দেখেছেন; CFG একই ধারণা, কিন্তু কোডের স্তরে আর কড়া নিয়মে।

  • Node: একটি basic block — পরপর চলা statement-এর দল, যাতে ঢোকার পথ একটি (প্রথম লাইন) আর বের হওয়ার পথ একটি (শেষ লাইন)। মাঝখানে কোনো লাফ নেই। তাই let a = 0; let b = 1; দুই লাইন হলেও একটি node.
  • Edge: তীর — একটি block শেষ হলে নিয়ন্ত্রণ কোন block-এ যেতে পারে।
  • Decision node (predicate node): যে node থেকে দুই বা ততোধিক তীর বের হয় — if, while, for-এর শর্ত, switch. দুই-মুখী হলে তীরে T (true) আর F (false) লেখা হয়।
  • Entry আর exit: function-এর শুরুর node আর শেষের node. ভালোভাবে গঠিত function-এ একটি করে।

প্রতিটি গঠনের ছবি নির্দিষ্ট: if (else ছাড়া) একটি decision, এক দিক body হয়ে নিচে মেলে, অন্য দিক সরাসরি নিচে; if/else-এ দুই দিক দুই body পেরিয়ে মেলে; while-এ শর্তের node থেকে T দিক body-তে, body শেষে একটি back edge আবার শর্তে, আর F দিক loop-এর বাইরে। for (init; cond; step) আসলে init + while (cond) { body; step }. নিচের কাল্পনিক function-টি একটি পরীক্ষার ফলের তালিকা থেকে পাস আর পূর্ণ নম্বর পাওয়া শিক্ষার্থী গোনে। Stepper-এ বাঁয়ে কোডের লাইন, ডানে CFG ধাপে ধাপে তৈরি হচ্ছে:

1function summary(marks) {2  let pass = 0, full = 0, i = 0;3  while (i < marks.length) {4    if (marks[i] >= 40) {5      pass++;6    }7    if (marks[i] === 100) {8      full++;9    }10    i++;11  }12  return { pass, full };13}
summary(marks)-এর control-flow graph, ধাপে ধাপে T F T F T F loop 1 2 3 4 5 6 7 8 entry exit
  1. Node ১ — শুরুর block. লাইন ২-এর তিনটি ঘোষণা পরপর চলে, মাঝে কোনো লাফ নেই — তাই একটি node. এটিই entry.
  2. Node ২ — loop-এর শর্ত. while (i < marks.length) একটি decision node: এখান থেকে দুটি তীর বের হবে। তীর ১ → ২ যোগ হলো। কমলা রং মানে decision.
  3. Node ৩ আর ৪ — প্রথম if. শর্ত সত্য (T) হলে node ২ থেকে ৩-এ ঢোকা যায়। Node ৩ (marks[i] >= 40) আরেকটি decision: T দিকে node ৪ (pass++). F দিকের তীর যাবে পরের if-এ, যা এখনো আঁকা হয়নি।
  4. Node ৫ আর ৬ — দ্বিতীয় if. দুই if-এর পথ node ৫-এ মেলে: ৪ → ৫ আর ৩ → ৫ (F). Node ৫ (marks[i] === 100) তৃতীয় decision; T দিকে node ৬ (full++).
  5. Node ৭ আর back edge. i++ একটি block; ৬ → ৭ আর ৫ → ৭ (F) এখানে মেলে। তারপর loop-এর শেষ — back edge ৭ → ২, আবার শর্তে ফেরা। এই back edge-ই loop-এর চিহ্ন।
  6. Node ৮ — exit. Loop-এর শর্ত মিথ্যা (F) হলে ২ → ৮, return. ছবি সম্পূর্ণ: N = ৮টি node, E = ১০টি edge, আর decision node ৩টি (২, ৩, ৫)। পরের অংশে এই সংখ্যাগুলোই কাজে লাগবে।

এবার একই graph-এর পূর্ণ ছবি, region-সহ। Graph-কে কাগজে এমনভাবে আঁকা যায় যাতে কোনো তীর অন্যটিকে কাটে না (planar graph); তখন তীরগুলো কাগজকে কয়েকটি বদ্ধ এলাকায় ভাগ করে, আর বাইরের অসীম এলাকাটিও একটি region হিসেবে গোনা হয়।

summary(marks)-এর CFG — node, decision আর region

Control-flow graph with numbered nodes (decision nodes in amber) and regions R1–R4

CFG: ৮টি node, ১০টি edge, ৩টি decision node, ৪টি region T F T F T F loop 1 2 3 4 5 6 7 8 R1 R2 R3 R4 (বাইরে) entry exit
ছবি ৫০.১ — কমলা node (২, ৩, ৫) decision; সবুজ T আর লাল F দুই দিক। বেগুনি R1–R3 তীরে ঘেরা তিনটি বদ্ধ এলাকা, R4 বাইরের এলাকা। Node-এর অর্থ: ১ = লাইন ২, ২ = লাইন ৩, ৩ = লাইন ৪, ৪ = লাইন ৫, ৫ = লাইন ৭, ৬ = লাইন ৮, ৭ = লাইন ১০, ৮ = লাইন ১২.

যুক্ত শর্ত (&&, ||) কীভাবে আঁকবেন? C, Java, JavaScript, Python-এ এগুলো short-circuit: a && b-এ a মিথ্যা হলে b দেখাই হয় না। তাই নিখুঁত CFG-তে if (a && b) দুটি decision node — প্রথমে a, সত্য হলে b. Decision-স্তরের ছবিতে একটি node-ও আঁকা হয়। কোনটি আঁকছেন, পরীক্ষার খাতায় স্পষ্ট লিখুন; অংশ ৬-এ দেখবেন এতে complexity-র সংখ্যা বদলায়।

৩ · Statement আর branch coverage — হিসাব আর পার্থক্য

প্রথম দুটি মাপকাঠি সবচেয়ে প্রচলিত, আর বেশিরভাগ tool এগুলোই দেখায়।

Statement coverage

Statement coverage: test suite চালালে প্রতিটি executable statement অন্তত একবার চলেছে কি না। CFG-তে: প্রতিটি node ছোঁয়া। মাপ = চলা statement ÷ মোট statement. অনেক tool লাইন গোনে (line coverage) — এক লাইনে দুটি statement থাকলে সামান্য পার্থক্য হয়।

Branch / decision coverage

Branch coverage: প্রতিটি decision-এর প্রতিটি ফল (T আর F) অন্তত একবার নেওয়া হয়েছে কি না। CFG-তে: decision node থেকে বের হওয়া প্রতিটি edge ছোঁয়া। মাপ = নেওয়া শাখা ÷ মোট শাখা। Decision coverage প্রায় একই নাম; কিছু মানে (যেমন DO-178C) entry আর exit point-ও যোগ হয়।

পার্থক্য সবচেয়ে বেশি দেখা যায় else ছাড়া if-এ। নিচে একটি কাল্পনিক ফল-নির্ধারণ function: বোনাস থাকলে ৫ নম্বর যোগ, ১০০-এর বেশি হলে ১০০-তে আটকানো, তারপর ৪০ বা তার বেশি হলে PASS. প্রোগ্রামটি Python-এর sys.settrace দিয়ে প্রতিটি চলা লাইন রেকর্ড করে — coverage tool ভেতরে এমনই একটি hook ব্যবহার করে। Branch বোঝা হয় পরপর দুটি চলা লাইনের জোড়া (arc) দেখে: decision লাইনের পরে body-র লাইন চললে T, অন্য লাইনে লাফ দিলে F. তিনটি test suite মাপা হচ্ছে:

import sys
SRC = '''def grade(marks, bonus):
    if bonus:
        marks = marks + 5
    if marks > 100:
        marks = 100
    if marks >= 40:
        return "PASS"
    return "FAIL"
'''
ns = {}
exec(compile(SRC, "grade.py", "exec"), ns)
STMTS = {2, 3, 4, 5, 6, 7, 8}                 # executable lines of the body
BRANCH = {2: (3, 4), 4: (5, 6), 6: (7, 8)}    # decision line: (True-next, False-next)
def measure(tests):
    lines, arcs, prev = set(), set(), [None]
    def tracer(frame, event, arg):
        if frame.f_code.co_name != "grade": return None
        if event == "call": prev[0] = None
        if event == "line":
            lines.add(frame.f_lineno)
            if prev[0]: arcs.add((prev[0], frame.f_lineno))
            prev[0] = frame.f_lineno
        return tracer
    sys.settrace(tracer)
    for m, b in tests: ns["grade"](m, b)
    sys.settrace(None)
    taken = {(d, o) for d, tf in BRANCH.items() for o, nxt in zip("TF", tf) if (d, nxt) in arcs}
    missed = sorted({(d, o) for d in BRANCH for o in "TF"} - taken)
    s, b = len(lines & STMTS), len(taken)
    print(f"tests={tests}\n  lines run {sorted(lines & STMTS)}  missed {sorted(STMTS - lines)}")
    print(f"  missed branches {missed}\n  statement {s}/7 = {100*s/7:.1f}%   branch {b}/6 = {100*b/6:.1f}%")

for suite in ([(50, True)], [(98, True), (30, True)], [(98, True), (30, False)]):
    measure(suite)

আউটপুট:

tests=[(50, True)]
  lines run [2, 3, 4, 6, 7]  missed [5, 8]
  missed branches [(2, 'F'), (4, 'T'), (6, 'F')]
  statement 5/7 = 71.4%   branch 3/6 = 50.0%
tests=[(98, True), (30, True)]
  lines run [2, 3, 4, 5, 6, 7, 8]  missed []
  missed branches [(2, 'F')]
  statement 7/7 = 100.0%   branch 5/6 = 83.3%
tests=[(98, True), (30, False)]
  lines run [2, 3, 4, 5, 6, 7, 8]  missed []
  missed branches []
  statement 7/7 = 100.0%   branch 6/6 = 100.0%

Suite ১, (50, True): বোনাস যোগ (লাইন ৩), ৫৫ ≤ ১০০ তাই লাইন ৫ বাদ, ৫৫ ≥ ৪০ তাই PASS (লাইন ৭), লাইন ৮ বাদ। Statement ৫/৭ = ৭১.৪%. Branch: লাইন ২-এর T, ৪-এর F, ৬-এর T — ছয়টির তিনটি, ৫০%. Suite ২: (98, True) ১০৩ হয়ে ১০০-তে আটকায়, (30, True) ৩৫ হয়ে FAIL — সাতটি লাইনই চলেছে, statement ১০০%! কিন্তু দুটি test-এই bonus সত্য, তাই (2, 'F') — “বোনাস ছাড়া” পথ কখনো নেওয়া হয়নি; branch ৫/৬ = ৮৩.৩%. এই না-নেওয়া পথ কোনো আলাদা লাইন নয়, শুধু একটি তীর — তাই statement coverage তাকে দেখতেই পায় না। ধরুন কোডটি এমন হতো: if bonus: extra = 5, তারপর marks = marks + extra. বোনাস ছাড়া ডাকলেই extra না থাকায় crash — অথচ Suite ২-এর মতো test-এ statement coverage ১০০% দেখাত। Suite ৩: দ্বিতীয় test-এ bonus=False — সব শাখা, ১০০%. খেয়াল করুন, শুধু ইনপুট বদলেছে, test-এর সংখ্যা একই। খেলনার সীমা: decision-এর T/F পরের লাইন হাতে লেখা (BRANCH); আসল tool এটি bytecode বা syntax tree থেকে বের করে, আর একই লাইনে লেখা শর্ত (x if c else y) সামলায়।

Subsumption — প্রথম ধাপ

মাপকাঠি A যদি মাপকাঠি B-কে subsume করে, তাহলে A পূরণ করা যেকোনো test suite B-ও পূরণ করে। Branch coverage statement coverage-কে subsume করে — যদি প্রতিটি শাখা নেওয়া হয়, প্রতিটি শাখার শেষে থাকা প্রতিটি block-ও চলে (শর্ত: প্রতিটি node কোনো না কোনো শাখার পথে আছে, যা সাধারণ গঠিত কোডে সত্য)। উল্টোটা সত্য নয় — Suite ২ তার প্রমাণ।

৪ · শর্তের স্তরে — condition, condition/decision, MC/DC আর multiple-condition

একটি decision (if-এর পুরো শর্ত) কয়েকটি condition-এর তৈরি হতে পারে — &&, || দিয়ে জোড়া, যেখানে প্রতিটি condition নিজে আর ভাঙা যায় না (যেমন marks >= 40, absent). Branch coverage শুধু পুরো decision-এর ফল দেখে; ভেতরের কোন condition কী করল, দেখে না। তাই শর্তের স্তরে চারটি মাপকাঠি, দুর্বল থেকে কড়া:

মাপকাঠিসংজ্ঞা — test suite-এ অবশ্যইif (a && b)-এ ন্যূনতম test
Condition coverageপ্রতিটি condition অন্তত একবার T আর একবার F হবে২: (T,F), (F,T) — কিন্তু দুটিতেই decision F!
Condition/decision coverage (C/DC)Condition coverage + প্রতিটি decision-এর দুই ফল২: (T,T), (F,F)
MC/DC (modified condition/decision coverage)C/DC + প্রতিটি condition দেখাবে যে বাকিগুলো স্থির রেখে শুধু তাকে বদলালে decision-এর ফল বদলায়৩: (T,T), (F,T), (T,F)
Multiple-condition coverage (MCC)Condition-গুলোর মানের প্রতিটি সম্ভাব্য সমন্বয়৪: 2²

প্রথম সারির ফাঁদটি পরীক্ষায় খুব আসে: condition coverage, decision coverage-কে subsume করে না। (T,F) আর (F,T)-এ a আর b দুটিই দুই মান পেয়েছে, অথচ a && b দুবারই মিথ্যা — if-এর body একবারও চলেনি, তাই statement coverage-ও পূরণ হয়নি। এজন্যই C/DC-তে দুটিকে একসাথে চাওয়া হয়।

MC/DC — প্রতিটি শর্তের নিজস্ব প্রভাব

MC/DC-র মূল কথা হলো independence pair: দুটি test, যেখানে শুধু একটি condition আলাদা, আর decision-এর ফলও আলাদা। এমন জোড়া থাকলে প্রমাণ হয় ওই condition-টি সত্যিই ফলাফলে প্রভাব রাখে — কোডে সেটি থাকা অর্থহীন নয়, আর ভুল লিখলে কোনো না কোনো test ধরবে। নিচের প্রোগ্রাম (a and b) or c-এর আটটি সমন্বয় তৈরি করে, প্রতিটি condition-এর independence pair খোঁজে, তারপর সবচেয়ে ছোট test set বের করে যেখানে তিনটি condition-এরই অন্তত একটি জোড়া আছে:

from itertools import product, combinations

def D(a, b, c):                       # the decision under test
    return (a and b) or c

rows = list(product([0, 1], repeat=3))          # all 8 combinations (a, b, c)
out = {r: int(bool(D(*r))) for r in rows}
for i, r in enumerate(rows, 1):
    print(f"T{i}: a={r[0]} b={r[1]} c={r[2]} -> {out[r]}")

# independence pairs: only condition k differs AND the outcome differs
pairs = {k: [] for k in range(3)}
for r, s in combinations(rows, 2):
    diff = [k for k in range(3) if r[k] != s[k]]
    if len(diff) == 1 and out[r] != out[s]:
        pairs[diff[0]].append((rows.index(r) + 1, rows.index(s) + 1))
for k, name in enumerate("abc"):
    print(f"{name} independence pairs: {pairs[k]}")

def is_mcdc(tests):                    # every condition shown by a pair inside the set
    return all(any(p in tests and q in tests for p, q in pairs[k]) for k in range(3))

for size in range(1, 9):
    found = [t for t in combinations(range(1, 9), size) if is_mcdc(set(t))]
    if found:
        print(f"smallest MC/DC set size = {size} (n+1 = {3 + 1}); all of them: {found}")
        break
print(f"multiple-condition coverage needs 2^3 = {len(rows)} tests")

আউটপুট:

T1: a=0 b=0 c=0 -> 0
T2: a=0 b=0 c=1 -> 1
T3: a=0 b=1 c=0 -> 0
T4: a=0 b=1 c=1 -> 1
T5: a=1 b=0 c=0 -> 0
T6: a=1 b=0 c=1 -> 1
T7: a=1 b=1 c=0 -> 1
T8: a=1 b=1 c=1 -> 1
a independence pairs: [(3, 7)]
b independence pairs: [(5, 7)]
c independence pairs: [(1, 2), (3, 4), (5, 6)]
smallest MC/DC set size = 4 (n+1 = 4); all of them: [(3, 4, 5, 7), (3, 5, 6, 7)]
multiple-condition coverage needs 2^3 = 8 tests

a-র একমাত্র জোড়া T3–T7: b=1, c=0 স্থির রেখে a ০ থেকে ১ হলে ফল ০ থেকে ১. অন্য সব অবস্থায় a বদলালে ফল বদলায় না — c=1 হলে or এমনিতেই সত্য, আর b=0 হলে and এমনিতেই মিথ্যা; একে বলে masking. b-রও একটিই জোড়া (T5–T7). c-র তিনটি, কারণ a and b মিথ্যা থাকলে c-ই ফল ঠিক করে। ন্যূনতম set: T7 (a, b দুটির জোড়াতেই লাগে), T3 আর T5, আর c-র জন্য T3-এর সাথী T4 অথবা T5-এর সাথী T6 — মোট ৪ = n + 1, যেখানে n = ৩টি condition. পূর্ণ সমন্বয় (MCC) চাইত ৮টি। Condition যত বাড়ে, এই ব্যবধান তত বাড়ে: ১০টি condition-এ MC/DC সাধারণত ১১টির কাছাকাছি test-এ হয় (গঠনভেদে কিছু বেশি লাগতে পারে), আর MCC-তে 2¹⁰ = ১০২৪টি। এখানে প্রতিটি জোড়ায় শুধু একটি condition বদলেছে — একে বলে unique-cause MC/DC; short-circuit ভাষার জন্য “masking MC/DC” নামে একটি শিথিল রূপও স্বীকৃত।

MC/DC আর আকাশ: বিমানের software-এর নির্দেশিকা RTCA DO-178B (১৯৯২) আর তার উত্তরসূরি DO-178C (২০১১) software-কে ব্যর্থতার পরিণতি অনুযায়ী স্তরে ভাগ করে। সাধারণভাবে উদ্ধৃত রূপে, সবচেয়ে গুরুতর স্তর Level A (ব্যর্থতা মানে বিপর্যয়)-এর জন্য structural coverage হিসেবে MC/DC চাওয়া হয়, Level B-তে decision coverage, Level C-তে statement coverage. খুঁটিনাটি নির্দেশিকা আর certification কর্তৃপক্ষের ব্যাখ্যায় নির্ভর করে — এখানে মোটা দাগে বলা হলো। গাড়ির নিরাপত্তা মান ISO 26262-ও সর্বোচ্চ ঝুঁকির স্তরে MC/DC-কে জোরালোভাবে সুপারিশ করে বলে উদ্ধৃত হয়। DO-178C-এর পুরো বিষয়টি NASA-র ২০০১ সালের “A Practical Tutorial on Modified Condition/Decision Coverage” (Hayhurst ও সহকর্মীরা)-এ বিস্তারিত।

৫ · Path coverage আর subsumption-এর সিঁড়ি

Path coverage সবচেয়ে কড়া structural মাপকাঠি: entry থেকে exit পর্যন্ত প্রতিটি সম্ভাব্য পথ অন্তত একবার। সমস্যা দুটি। প্রথমত, বিস্ফোরণ: পরপর ১০টি স্বাধীন if থাকলে পথ 2¹⁰ = ১০২৪টি; আর loop থাকলে — summary-এর loop যতবার খুশি ঘুরতে পারে, প্রতিটি ঘূর্ণনে ৪ রকম ভেতরের পথ — ঘূর্ণন সীমাহীন হলে পথও অসীম। দ্বিতীয়ত, infeasible path: graph-এ আঁকা কিছু পথ বাস্তবে কোনো ইনপুটেই চলে না, কারণ শর্তগুলো পরস্পর-নির্ভর (অনুশীলন ২-এ একটি দেখবেন)। তাই বাস্তবে path coverage-এর বদলে সীমিত রূপ ব্যবহৃত হয়: loop শূন্য, এক আর বহুবার (boundary interior), অথবা পরের অংশের basis path.

সব মাপকাঠি একসাথে, subsumption-এর তীর দিয়ে। তীর A → B মানে A পূরণ হলে B-ও পূরণ:

Coverage মাপকাঠির subsumption সিঁড়ি

Subsumption hierarchy of structural coverage criteria (arrow = “subsumes”)

Subsumption: MCC → MC/DC → C/DC → condition ও branch; path → basis path → branch → statement Multiple-condition Path MC/DC Basis path Condition/decision Condition Branch / decision Statement সব সমন্বয় · 2ⁿ সব পথ · loop-এ অসীম প্রতিটি শর্তের প্রভাব V(G)টি স্বাধীন পথ শর্ত + decision প্রতিটি শর্ত T, F প্রতিটি edge প্রতিটি node ✗ branch বা statement নিশ্চিত করে না
ছবি ৫০.২ — বাঁ স্তম্ভ শর্তের দিক, ডান স্তম্ভ পথের দিক; দুটি মেলে branch coverage-এ। Condition coverage একা দাঁড়িয়ে — সে কাউকে subsume করে না। Path আর multiple-condition তুলনীয় নয়: একটি সব পথ চায়, অন্যটি প্রতিটি decision-এর ভেতরের সব সমন্বয়। ধরে নেওয়া হয়েছে প্রতিটি শাখা আর শর্তের মান বাস্তবে পাওয়া সম্ভব।

এক নজরে, পরীক্ষার জন্য একটি ছোট নিয়ম: “কোন মাপকাঠিতে সবচেয়ে কম কয়টি test লাগে” প্রশ্নে প্রথমে CFG আঁকুন, তারপর প্রতিটি লক্ষ্য (node, edge, শর্তের মান, জোড়া) ঢাকতে একটি test কতগুলো লক্ষ্য একসাথে ছুঁতে পারে ভাবুন — আর পরস্পর-নির্ভর শর্তের কারণে কোনো সমন্বয় অসম্ভব কি না যাচাই করুন।

৬ · Cyclomatic complexity — এক graph, তিন হিসাব

১৯৭৬ সালে Thomas J. McCabe IEEE Transactions on Software Engineering-এ “A Complexity Measure” প্রবন্ধে graph তত্ত্ব থেকে একটি সংখ্যা আনেন: cyclomatic complexity, V(G). অর্থ: CFG-তে কতগুলো linearly independent path আছে — এমন পথের সবচেয়ে বড় দল, যার কোনোটি বাকিগুলোর edge জোড়াতালি দিয়ে বানানো যায় না। একই সংখ্যা তিনভাবে বের হয়:

V(G) = E − N + 2P  =  decision node + ১  =  region-এর সংখ্যা
E = edge, N = node, P = graph-এর আলাদা অংশ (একটি function-এ P = ১)
  • E − N + 2P সব সময় খাটে। কয়েকটি function একসাথে (P = ৩) হলে প্রতিটির V(G) যোগ করলেই মোট।
  • Decision + ১ খাটে single-entry, single-exit function-এ যেখানে প্রতিটি decision দুই-মুখী। switch-এ k-টি শাখা থাকলে সেটি k − ১টি দুই-মুখী decision-এর সমান।
  • Region-এর সংখ্যা খাটে graph planar হলে (কোনো তীর অন্যটিকে না কেটে আঁকা যায়); বাইরের এলাকাসহ গুনতে হয়। এটি Euler-এর সূত্র (N − E + F = ২) থেকে আসে।

নিচের stepper-এ ছবি ৫০.১-এর graph-এ তিনটি হিসাব, তারপর basis set-এর চারটি পথ:

Cyclomatic complexity আর basis path, ধাপে ধাপে R1R2R3R4(বাইরে) T F T F T F loop 1 2 3 4 5 6 7 8 N = 8 · E = 10 entry exit
হিসাবের খাতা
পদ্ধতি
হিসাব
ফল
  1. প্রথম হিসাব: E − N + 2P. তীর গুনুন: ১→২, ২→৩, ২→৮, ৩→৪, ৩→৫, ৪→৫, ৫→৬, ৫→৭, ৬→৭, ৭→২ — মোট ১০. Node ৮টি, function একটি তাই P = ১. V(G) = ১০ − ৮ + ২ = ৪.
  2. দ্বিতীয় হিসাব: decision + ১. যে node থেকে দুটি তীর বের হয়: ২ (while), ৩ (প্রথম if), ৫ (দ্বিতীয় if) — তিনটি, সব দুই-মুখী। V(G) = ৩ + ১ = ৪. কোডেই গোনা যায়: while আর দুটি if.
  3. তৃতীয় হিসাব: region. তীরে ঘেরা বদ্ধ এলাকা: R1 (loop-এর back edge আর মাঝের খুঁটি ২-৩-৫-৭-এর মাঝে), R2 (ত্রিভুজ ৩-৪-৫), R3 (ত্রিভুজ ৫-৬-৭)। বাইরের এলাকা R4 যোগ করে ৪. তিন হিসাবই মিলল।
  4. Basis P1 — baseline. McCabe-এর baseline পদ্ধতি: একটি স্বাভাবিক পথ বেছে নিন — loop-এ ঢোকা, পাস, পূর্ণ নম্বর নয়, তারপর loop থেকে বের হওয়া। ইনপুট [50]. প্রত্যাশিত ফল: pass 1, full 0.
  5. Basis P2 — node ২ উল্টানো. Baseline-এর প্রথম decision (loop-এর শর্ত) উল্টো দিকে: সরাসরি ২ → ৮. ইনপুট খালি তালিকা [] — “শূন্যবার loop”, সীমার একটি গুরুত্বপূর্ণ case.
  6. Basis P3 — node ৩ উল্টানো. শুধু প্রথম if-এর দিক বদল: ৩ → ৫ (F). ইনপুট [30] — ফেল, পূর্ণ নম্বরও নয়।
  7. Basis P4 — node ৫ উল্টানো. শুধু দ্বিতীয় if: ৫ → ৬ (T). ইনপুট [100]. চারটি পথ = V(G). একসাথে এরা প্রতিটি edge অন্তত একবার ছোঁয়, তাই branch coverage-ও পূর্ণ।

নিচের JavaScript একই graph হাতে লিখে (node → পরের node-এর তালিকা) তিনটি হিসাব করে, baseline পদ্ধতিতে basis set বানায়, আর প্রমাণ করে যে পথগুলো সত্যিই স্বাধীন: প্রতিটি পথকে ১০টি edge-এর গোনা দিয়ে একটি vector বানিয়ে তাদের rank (Gaussian elimination) বের করে।

// CFG of summary(marks), drawn by hand: node -> [trueNext, falseNext] or [next]
const G = { 1: [2], 2: [3, 8], 3: [4, 5], 4: [5], 5: [6, 7], 6: [7], 7: [2], 8: [] };
const N = Object.keys(G).length, P = 1;        // one function = one connected part
const E = Object.values(G).reduce((s, out) => s + out.length, 0);
const decisions = Object.values(G).filter(out => out.length === 2).length;
// faces of the planar drawing (Figure 50.2), listed by hand: 3 inside + outer
const faces = ["R1: 2-3-5-7-2", "R2: 3-4-5-3", "R3: 5-6-7-5", "R4: outer"];
console.log(`N=${N} E=${E} P=${P} decisions=${decisions}`);
console.log(`(1) E - N + 2P   = ${E - N + 2 * P}`);
console.log(`(2) decisions + 1 = ${decisions + 1}`);
console.log(`(3) regions       = ${faces.length}  (Euler check E-N+2 = ${E - N + 2})`);
// Basis set, McCabe's baseline method: flip one decision at a time.
const base = { 2: 0, 3: 0, 5: 1 };             // baseline: enter loop, pass, not full
function walk(flip) {
  const path = [1], seen = new Set();          // loop runs at most once per path
  for (let n = 1; G[n].length; path.push(n)) {
    const k = G[n].length < 2 ? 0 : seen.has(n) ? 1 : n === flip ? 1 - base[n] : base[n];
    seen.add(n); n = G[n][k];
  }
  return path;
}
const basis = [walk(null), ...Object.keys(base).map(d => walk(Number(d)))];
const edgeVec = p => Object.entries(G).flatMap(([a, out]) =>
  out.map(b => p.slice(1).filter((x, i) => p[i] == a && x == b).length));
function rank(m) {                              // Gaussian elimination
  let r = 0;
  for (let c = 0; c < m[0].length && r < m.length; c++) {
    const p = m.findIndex((row, i) => i >= r && row[c] !== 0); if (p < 0) continue;
    [m[r], m[p]] = [m[p], m[r]];
    m = m.map((row, i) => i === r ? row : row.map((v, j) => v - row[c] / m[r][c] * m[r][j])); r++;
  }
  return r;
}
basis.forEach((p, i) => console.log(`basis P${i + 1}: ${p.join("-")}`));
console.log(`rank of edge vectors = ${rank(basis.map(edgeVec))}`);

আউটপুট:

N=8 E=10 P=1 decisions=3
(1) E - N + 2P   = 4
(2) decisions + 1 = 4
(3) regions       = 4  (Euler check E-N+2 = 4)
basis P1: 1-2-3-4-5-7-2-8
basis P2: 1-2-8
basis P3: 1-2-3-5-7-2-8
basis P4: 1-2-3-4-5-6-7-2-8
rank of edge vectors = 4

তিন হিসাবই ৪. Region-এর তালিকা ছবি থেকে হাতে লেখা (কোড নিজে ছবি আঁকে না), আর Euler-এর সূত্র দিয়ে মিলিয়ে দেখা হয়েছে। walk প্রতিটি decision-এ baseline-এর দিক নেয়, শুধু flip করা node-এ উল্টো দিক; loop-এর শর্তে দ্বিতীয়বার পৌঁছালে বের হয়ে যায় — তাই প্রতিটি পথে loop সর্বোচ্চ একবার ঘোরে। শেষ লাইনে rank = ৪: চারটি edge-vector-এর কোনোটি বাকিদের যোগ-বিয়োগে বানানো যায় না। আরেকটি পথ নিন, যেমন [50, 30]-এর পথ (loop দুবার) — তার vector এই চারটির একটি সমন্বয়, তাই নতুন “স্বাধীন” পথ নয়। সতর্কতা: baseline পদ্ধতি কখনো infeasible পথ দেয়; তখন অন্য baseline বা অন্য উল্টানো ক্রম নিয়ে চেষ্টা করতে হয়।

Basis path testing — ধাপগুলো

  1. Code থেকে CFG আঁকা (যুক্ত শর্ত ভেঙে, যদি condition-স্তরে চান)।
  2. V(G) হিসাব — অন্তত দুই পদ্ধতিতে, মিলিয়ে নিতে।
  3. V(G)টি স্বাধীন পথের একটি basis set বের করা (baseline + একটি করে decision উল্টানো)।
  4. প্রতিটি পথ চালাবে এমন ইনপুট বেছে নেওয়া; infeasible হলে পথ বদলানো।
  5. প্রতিটি ইনপুটের প্রত্যাশিত ফল specification থেকে লেখা — কোড থেকে নয়।
  6. Test চালিয়ে আসল ফল প্রত্যাশার সাথে মেলানো।

McCabe-এর structured testing-এর দাবি: basis set চালানো test suite branch coverage নিশ্চিত করে, আর সাধারণত branch coverage-এর ন্যূনতম suite-এর চেয়ে বেশি পথ-সমন্বয় দেখে। তাই V(G) হলো basis path testing-এর test-সংখ্যা — আর branch coverage-এর জন্য দরকারি ন্যূনতম test-এর একটি ঊর্ধ্বসীমা (অনেক সময় কম test-এই সব edge ছোঁয়া যায়: summary-এ একটিমাত্র ইনপুট [100, 30]-ই সব ১০টি edge ছোঁয়)।

কত হলে বেশি?

McCabe তাঁর ১৯৭৬ সালের প্রবন্ধে একটি module-এর ঊর্ধ্বসীমা হিসেবে ১০ প্রস্তাব করেন, আর NIST-এর ১৯৯৬ সালের প্রকাশনা SP 500-235 (Watson ও McCabe, “Structured Testing”) সাধারণভাবে ১০-কে ভালো সূচনা বলে, অভিজ্ঞ দল আর ভালো test থাকলে ১৫ পর্যন্ত মেনে নেওয়ার কথাও বলে। এটি বৈজ্ঞানিক নিয়ম নয়, অভিজ্ঞতার রীতি। Tool-এর default আলাদা আর সময়ে বদলায় — যেমন লেখার সময়ে ESLint-এর complexity নিয়মের default সীমা ২০ বলে জানি; Python-এর radon-এর মতো tool ১–৫ = A, ৬–১০ = B ধাঁচে শ্রেণি দেয়। সংখ্যা মুখস্থ না করে দলের সীমা config-এ লিখে রাখুন, আর সীমা ছাড়ালে function ভাঙার কথা ভাবুন (পর্ব ৪২-এর refactoring).

হিসাবের পার্থক্য tool-ভেদে। কেউ &&, ||-কে আলাদা decision ধরে (condition-স্তর, McCabe-এর পরামর্শও এমন), কেউ ধরে না; কেউ switch-এর প্রতিটি case-এ +১, কেউ পুরো switch-এ +১; ternary ?:, catch, ?? নিয়েও মতভেদ। তাই দুটি tool-এর সংখ্যা সরাসরি তুলনা করবেন না, আর পরীক্ষার উত্তরে নিজের নিয়মটি এক লাইনে লিখে দিন।

৭ · Cognitive complexity আর অন্য মাপ — কোনটি কী বলে

Cyclomatic complexity মূলত test-এর প্রশ্নের উত্তর দেয়: কতগুলো স্বাধীন পথ। কিন্তু মানুষের কাছে কোড কতটা কঠিন, তা শুধু পথের সংখ্যায় ধরা পড়ে না। পর্ব ৪১-এ (Clean code) দেখেছেন guard clause দিয়ে nesting কমালে কোড সহজ হয়, অথচ cyclomatic সংখ্যা একই থাকে। এই ফাঁক পূরণে SonarSource ২০১৭ সালের দিকে G. Ann Campbell-এর লেখা একটি white paper-এ cognitive complexity প্রস্তাব করে। মূল নিয়ম তিনটি (সংক্ষেপে; পূর্ণ নিয়ম white paper-এ):

  1. সরল প্রবাহ ভাঙলে +১: if, else if, else, loop, catch, switch (পুরোটায় একবার), একই ধরনের যুক্ত শর্তের প্রতিটি ধারা (a && b && c = +১, a && b || c = +২), recursion.
  2. Nesting-এর জন্য বাড়তি: ভেতরে বসানো প্রতিটি if, loop বা catch তার nesting-স্তরের সমান বাড়তি পায়।
  3. যা পড়া সহজ করে, তাতে কিছু যোগ হয় না: একটি switch-এর অনেক case, বা ছোট শর্তের সংক্ষিপ্ত রূপ।
// Flat: cyclomatic 4, cognitive 3
function flatFee(a, b, c) {
  let fee = 0;
  if (a) fee += 10;            // +1
  if (b) fee += 20;            // +1
  if (c) fee += 30;            // +1
  return fee;
}
// Nested: cyclomatic 4, cognitive 6
function nestedFee(a, b, c) {
  let fee = 0;
  if (a) {                     // +1
    if (b) {                   // +1 +1 (nesting level 1)
      if (c) fee = 60;         // +1 +2 (nesting level 2)
    }
  }
  return fee;
}

দুটিতেই তিনটি দুই-মুখী decision, তাই cyclomatic ৪ = ৩ + ১. কিন্তু cognitive হিসাবে প্রথমটি ১ + ১ + ১ = ৩, দ্বিতীয়টি ১ + ২ + ৩ = ৬. তিন স্তর ভেতরে গিয়ে পাঠককে তিনটি শর্ত একসাথে মাথায় রাখতে হয় — সংখ্যাটি ঠিক সেই কষ্টকে ধরতে চায়। উল্টো দিকে, ১০টি case-এর একটি switch-এ cyclomatic ১১ (প্রতিটি case-কে +১ ধরলে), কিন্তু cognitive মাত্র ১ — কারণ সারি সারি case পড়া সহজ। SonarQube-এর মতো tool-এর নিয়মে লেখার সময়ে function-প্রতি cognitive সীমার default ১৫ বলে জানি; সংস্করণভেদে যাচাই করে নিন।

মাপকী গোনেকোন প্রশ্নের উত্তর
Cyclomatic complexityস্বাধীন পথ (decision + ১)কতগুলো test case লাগতে পারে; শাখার ঘনত্ব
Cognitive complexityপ্রবাহের ভাঙন + nesting-এর ভারমানুষের পড়তে কত কষ্ট
Nesting depthসবচেয়ে গভীর ব্লক-স্তরকতগুলো শর্ত একসাথে মাথায় রাখতে হয়
লাইন সংখ্যা (LOC)লাইনআকার; অনেক গবেষণায় complexity-র সাথে জোরালো সম্পর্ক পাওয়া গেছে
Coupling (Ca, Ce), fan-in/fan-outmodule-এর মধ্যে নির্ভরতাএকটি বদলের প্রভাব কতদূর — পর্ব ২৫ (coupling আর cohesion)

কোনো একটি software metric একা কোডের মান বলে না। জটিলতার মাপ সবচেয়ে কাজের প্রবণতা আর তুলনা হিসেবে: কোন function গত ছয় মাসে ক্রমশ জটিল হচ্ছে, কোন ফাইলে complexity বেশি আর বদলও ঘনঘন (hotspot) — refactoring আর বাড়তি test-এর প্রথম প্রার্থী।

৮ · Coverage tool, target আর তার সীমা

বাস্তবে coverage হাতে মাপা হয় না। Tool কোড চালানোর সময় প্রতিটি লাইন বা শাখায় নজর রাখে — হয় interpreter-এর hook দিয়ে (Python-এ sys.settrace, নতুন সংস্করণে sys.monitoring), হয় কোডে গোনার নির্দেশ ঢুকিয়ে (instrumentation), হয় runtime-এর নিজস্ব হিসাব থেকে। লেখার সময়ের পরিচিত কয়েকটি:

  • Python — coverage.py: coverage run --branch -m pytest, তারপর coverage report -m (কোন লাইন বাদ পড়ল তার তালিকাসহ) বা coverage html. --branch না দিলে শুধু statement মাপে — এটি অনেকে ভুলে যান।
  • JavaScript: Istanbul (CLI-র নাম nyc) কোডে গোনার নির্দেশ ঢোকায়; c8 V8 engine-এর নিজস্ব coverage ব্যবহার করে। Jest-এ --coverage, Vitest-এ coverage provider বেছে নেওয়া যায়। এদের রিপোর্টে সাধারণত statements, branches, functions আর lines — চারটি আলাদা শতাংশ।
  • অন্য ভাষা: Java-তে JaCoCo, C/C++-এ gcov বা llvm-cov, .NET-এ coverlet — নাম আর সুবিধা সংস্করণভেদে বদলায়।
[tool.coverage.run]
branch = true          # measure branches, not only lines
source = ["grading"]

[tool.coverage.report]
show_missing = true
fail_under = 80        # exit non-zero below 80% (CI turns red)

এই config-এ CI (পর্ব ৫৮) coverage ৮০%-এর নিচে নামলেই pipeline লাল করবে — একটি quality gate. প্রশ্ন হলো, এমন gate কতটা ভালো?

Goodhart-এর ফাঁদ: ১০০% coverage, শূন্য যাচাই

অর্থনীতিবিদ Charles Goodhart ১৯৭৫ সালে মুদ্রানীতির প্রসঙ্গে একটি পর্যবেক্ষণ দেন, যা পরে নৃবিজ্ঞানী Marilyn Strathern-এর (১৯৯৭) ভাষায় জনপ্রিয় হয়: “যখন কোনো মাপ লক্ষ্যে পরিণত হয়, তা আর ভালো মাপ থাকে না।” এটি Goodhart’s law. Coverage-এ এর সবচেয়ে সোজা রূপ:

def test_grade_everything():
    grade(98, True)      # runs the clamp branch
    grade(30, False)     # runs the no-bonus and FAIL branches
    # no assert: 100% statement and 100% branch coverage, catches nothing

অংশ ৩-এর Suite ৩-এর ইনপুট, তাই coverage tool ১০০% দেখাবে। কিন্তু grade সব ক্ষেত্রে "PASS" ফেরত দিলেও এই test পাস করবে — এটি একটি assertion-free test. Coverage বলে লাইনটি চলেছে; ফল যাচাই হয়েছে কি না, বলে না। আরও কিছু সীমা:

  • বাদ পড়া কোড দেখা যায় না: requirement-এ আছে কিন্তু কেউ লেখেননি (ধরুন অনুপস্থিত শিক্ষার্থীর নিয়ম) — না-লেখা লাইনের coverage মাপা যায় না। এখানে কাজে লাগে পর্ব ৪৭-এর black-box কৌশল।
  • ইনপুটের মান দেখে না: marks >= 40-এর দুই শাখা ৯০ আর ১০ দিয়েও ঢাকা যায়; ভুল করে > 40 লেখা থাকলে ধরা পড়ে শুধু ৪০ দিয়ে — boundary value.
  • শেষের কয়েক শতাংশ সবচেয়ে দামি: ডিস্ক ভরে যাওয়া বা network ছিঁড়ে যাওয়ার error-handling পথ ঢাকতে test double লাগে (পর্ব ৪৯, Test double), আর জোর করে ১০০% চাইলে মানুষ ভঙ্গুর test লেখেন।

তাহলে target কীভাবে রাখবেন? অনেক দলের অভিজ্ঞতা: (১) মোট সংখ্যার চেয়ে নতুন বা বদলানো কোডের coverage দেখা (diff coverage) — পুরোনো legacy কোডের হাজার লাইন একসাথে ঢাকার চাপ থাকে না; (২) সংখ্যা “কমতে দেব না” ধাঁচের gate, যাতে ঢালু পথে না নামে; (৩) কম coverage-কে প্রশ্ন হিসেবে দেখা — “এই শাখা কেন ঢাকা নেই?” — শাস্তি হিসেবে নয়; (৪) টাকা-পয়সা বা নিরাপত্তার মতো ঝুঁকিপূর্ণ অংশে branch, এমনকি mutation-এর কড়া মাপ। কোনো নির্দিষ্ট শতাংশ সার্বজনীনভাবে “সঠিক” নয়।

৯ · Mutation testing — test-এর test

Coverage প্রশ্ন করে, “test কোডটি চালিয়েছে?” Mutation testing প্রশ্ন করে, “কোডে ভুল থাকলে test কি ধরত?” পদ্ধতিটি সরল: আসল কোডে একটিমাত্র ছোট পরিবর্তন করে একটি নতুন সংস্করণ — mutant — বানানো, তারপর পুরো test suite চালানো।

বাজারের ওজন-মাপার যন্ত্র ঠিক আছে কি না যাচাই করতে পরিদর্শক জেনেশুনে ৯৯০ গ্রামের একটি বাটখারা রাখেন। যন্ত্র “১ কেজি” দেখালে যন্ত্রটি দুর্বল। Mutation testing-এ বাটখারা হলো mutant, আর যন্ত্র হলো আপনার test suite: ইচ্ছে করে বসানো ভুল ধরতে না পারলে, আসল ভুলও হয়তো ধরবে না।

  • Mutation operator: পরিবর্তনের নিয়ম। Relational (>= → >, < → <=), arithmetic (+ → -, * → /), logical (&& → ||, শর্ত উল্টানো !), constant (40 → 41, 0 → 1), statement মুছে ফেলা, return-এর মান বদলানো।
  • Killed: অন্তত একটি test fail করেছে — test suite ভুলটি ধরেছে। ভালো খবর।
  • Survived: সব test পাস — ভুলটি কেউ ধরেনি। হয় test দুর্বল, নয়তো mutant-টি equivalent.
  • Equivalent mutant: লেখা আলাদা, কিন্তু কোনো ইনপুটেই আচরণ আলাদা নয় — কোনো test একে মারতে পারবে না। এটি চেনা সাধারণভাবে undecidable সমস্যা, তাই প্রায়ই মানুষকে দেখে ঠিক করতে হয়।
Mutation score = killed ÷ (মোট mutant − equivalent mutant) × ১০০

নিচের stepper-এ একটি কাল্পনিক নিয়ম: অনুপস্থিত হলে ফেল; নম্বর + বোনাস, সর্বোচ্চ ১০০; মোট ৪০ বা তার বেশি হলে পাস। Mutant M1-এ >= বদলে > করা হয়েছে:

1function isPass(marks, bonus, absent) {2  if (absent) return false;3  const total = Math.min(marks + bonus, 100);4  return total >= 40;5}M1  return total > 40;   // mutant
Test আর ফল
test
total
প্রত্যাশা
আসল
M1
রায়
  1. Mutant তৈরি। Tool লাইন ৪-এ relational operator >=-কে > করে M1 বানাল — বাস্তবের খুব চেনা off-by-one ভুল। এবার “দুর্বল” suite চালানো হবে: দুটি test, (75, 0, false) → true আর (20, 0, false) → false.
  2. প্রথম test: ৭৫. আসল কোডে ৭৫ ≥ ৪০ সত্য, M1-এ ৭৫ > ৪০-ও সত্য। প্রত্যাশা true — দুটিই মিলল, test পাস। এই test M1-কে আলাদা করতে পারে না।
  3. দ্বিতীয় test: ২০. ২০ ≥ ৪০ মিথ্যা, ২০ > ৪০-ও মিথ্যা। আবার পাস। পুরো suite পাস করল — M1 survived. লক্ষ করুন, এই দুটি test-এ লাইন ৪-এর দুই ফলই এসেছে, অর্থাৎ এই লাইনে coverage-এর চোখে কোনো ফাঁক নেই!
  4. Boundary test: ৪০. “শক্ত” suite-এ যোগ হলো (40, 0, false) → true. আসল কোডে ৪০ ≥ ৪০ সত্য, M1-এ ৪০ > ৪০ মিথ্যা। M1 ভুল ফল দিল, test fail — M1 killed. যে ইনপুটে দুই রূপ আলাদা হয়, সেটি ঠিক সীমার মান।
  5. Score. পাঁচটি mutant-এর (নিচের প্রোগ্রামে) একটি equivalent. দুর্বল suite মারে ১টি: ১ ÷ (৫ − ১) = ২৫%. শক্ত suite (আরও ৩টি test) মারে ৪টি: ৪ ÷ ৪ = ১০০%. দুর্বল suite-এও লাইন ৪-এর দুই ফল এসেছিল, তবু সীমার মানের ফাঁকটি দেখাল শুধু mutation.

এবার পাঁচটি mutant, চার ধরনের operator. প্রোগ্রামটি আসল function-এর লেখা (string) থেকে একটি অংশ বদলে mutant বানায়, প্রতিটির ওপর দুটি suite চালায়, আর score ছাপে। আসল tool-ও মূলত এমনই করে — তবে syntax tree বা bytecode-এর স্তরে:

// Fictional rule: absent -> fail; total = marks + bonus, capped at 100; pass if total >= 40
const ORIGINAL = `if (absent) return false;
const total = Math.min(marks + bonus, 100);
return total >= 40;`;
const MUTANTS = [                               // [name, operator, find, replace]
  ["M1", "relational  >= to >",    "total >= 40", "total > 40"],
  ["M2", "constant    40 to 41",   ">= 40", ">= 41"],
  ["M3", "arithmetic  + to -",     "marks + bonus", "marks - bonus"],
  ["M4", "logical     negate if",  "if (absent)", "if (!absent)"],
  ["M5", "constant    100 to 101", ", 100)", ", 101)"],
];
const make = body => new Function("marks", "bonus", "absent", body);
const isPass = make(ORIGINAL);
const WEAK = [[75, 0, false, true], [20, 0, false, false]];
const STRONG = [...WEAK, [40, 0, false, true], [38, 2, false, true], [90, 0, true, false]];
const EQUIVALENT = new Set(["M5"]);            // decided by a human: no input can tell M5 apart
function run(name, suite) {
  console.log(`--- ${name} suite (${suite.length} tests) ---`);
  for (const [m, b, a, want] of suite)          // the suite must pass on the original
    if (isPass(m, b, a) !== want) throw new Error("bad test");
  let killed = 0;
  for (const [id, op, find, repl] of MUTANTS) {
    const mutant = make(ORIGINAL.replace(find, repl));
    const failing = suite.find(([m, b, a, want]) => mutant(m, b, a) !== want);
    if (failing) killed++;
    const tag = failing ? `killed by (${failing.slice(0, 3)})` :
      EQUIVALENT.has(id) ? "survived (equivalent)" : "SURVIVED";
    console.log(`${id} ${op.padEnd(22)} ${tag}`);
  }
  const score = killed / (MUTANTS.length - EQUIVALENT.size);
  console.log(`mutation score = ${killed}/(${MUTANTS.length}-${EQUIVALENT.size}) = ${(100 * score).toFixed(0)}%`);
}
run("weak", WEAK);
run("strong", STRONG);

আউটপুট:

--- weak suite (2 tests) ---
M1 relational  >= to >    SURVIVED
M2 constant    40 to 41   SURVIVED
M3 arithmetic  + to -     SURVIVED
M4 logical     negate if  killed by (75,0,false)
M5 constant    100 to 101 survived (equivalent)
mutation score = 1/(5-1) = 25%
--- strong suite (5 tests) ---
M1 relational  >= to >    killed by (40,0,false)
M2 constant    40 to 41   killed by (40,0,false)
M3 arithmetic  + to -     killed by (38,2,false)
M4 logical     negate if  killed by (75,0,false)
M5 constant    100 to 101 survived (equivalent)
mutation score = 4/(5-1) = 100%

দুর্বল suite শুধু M4-কে মারে — শর্ত উল্টালে ৭৫ নম্বরের উপস্থিত শিক্ষার্থীও ফেল করে, সহজেই ধরা পড়ে। M1 আর M2 (৪০ → ৪১) বাঁচে, কারণ ৪০-এর কাছাকাছি কোনো ইনপুট নেই; M3 (+ → -) বাঁচে, কারণ কোনো test-এ বোনাস শূন্যের বেশি নয় — শূন্য যোগ আর শূন্য বিয়োগ একই। শক্ত suite-এ (40, 0, false) M1 আর M2 মারে, (38, 2, false) M3 মারে (৩৮ + ২ = ৪০ পাস, ৩৮ − ২ = ৩৬ ফেল)। (90, 0, true) অনুপস্থিতির নিয়ম যাচাই করে। M5 (১০০ → ১০১) equivalent: সীমা শুধু তখন কাজ করে যখন মোট ১০০ ছাড়ায়; তখন মোট ১০০ বা ১০১ — দুটিই ≥ ৪০, ফল একই। কোনো test একে মারতে পারবে না, তাই হর থেকে বাদ: ৪ ÷ (৫ − ১) = ১০০%. Equivalent তালিকাটি (EQUIVALENT) মানুষের বিচার, প্রোগ্রামের আবিষ্কার নয়। run শুরুতে যাচাই করে প্রতিটি test আসল কোডে পাস করছে — নইলে “killed” অর্থহীন হতো।

ইতিহাস: Mutation testing-এর ধারণা সাধারণত Richard Lipton-এর ১৯৭০-এর দশকের শুরুর কাজে খোঁজা হয়, আর বহুল উদ্ধৃত প্রবন্ধটি Richard DeMillo, Richard Lipton ও Frederick Sayward-এর “Hints on Test Data Selection: Help for the Practicing Programmer” (IEEE Computer, ১৯৭৮)। প্রায় একই সময়ে Richard Hamlet-ও স্বাধীনভাবে একই দিকে কাজ করেন। দুটি অনুমান এর ভিত্তি: competent programmer hypothesis (প্রোগ্রামার মোটামুটি ঠিক কোড লেখেন, ভুল সাধারণত ছোট) আর coupling effect (ছোট ভুল ধরতে পারা test বড়, জটিল ভুলের বেশিরভাগও ধরে)।

Tool আর খরচ

লেখার সময়ের পরিচিত tool: Java-তে PIT (PITest), JavaScript/TypeScript আর C#-এ Stryker-এর বিভিন্ন সংস্করণ, Python-এ mutmut বা cosmic-ray. নাম আর সুবিধা বদলায়; operator-এর তালিকা আর রিপোর্টের ধাঁচ documentation-এ মিলিয়ে নিন। খরচই বড় বাধা: প্রতিটি mutant-এ test suite চালাতে হয়। ১,০০০ লাইনের module-এ কয়েকশো mutant হতে পারে; suite ২ মিনিটের হলে ৫০০ mutant মানে সবচেয়ে খারাপ ক্ষেত্রে ১,০০০ মিনিট। কমানোর কৌশল: শুধু বদলানো লাইনে mutant (diff-ভিত্তিক), প্রতিটি mutant-এ শুধু সেই test যা ওই লাইন ছোঁয় (coverage তথ্য দিয়ে), প্রথম fail-এই থামা, নমুনা বেছে নেওয়া, আর রাতে বা সপ্তাহে একবার পূর্ণ run. আর survived mutant-এর তালিকা পড়ে সিদ্ধান্ত নিতে মানুষের সময়ও খরচ।

আরও গভীরে: Coverage-এর ক্লাসিক সংজ্ঞাগুলো (statement, decision, condition, decision/condition, multiple-condition) Glenford Myers-এর The Art of Software Testing (১৯৭৯)-এ সুসংহত রূপ পায়, আর Hong Zhu, Patrick Hall ও John May-এর জরিপ “Software Unit Test Coverage and Adequacy” (ACM Computing Surveys, ১৯৯৭) subsumption-এর পুরো মানচিত্র দেয়। McCabe-এর মূল প্রবন্ধে graph-কে strongly connected ধরতে exit থেকে entry-তে একটি কাল্পনিক তীর যোগ করে v = e − n + p লেখা হয়; ওই বাড়তি তীর না আঁকলে সেটিই E − N + 2P. Mutation testing-এর আধুনিক অবস্থা নিয়ে Yue Jia ও Mark Harman-এর জরিপ (IEEE TSE, ২০১১) পড়ার মতো।

পরীক্ষা ও ইন্টারভিউতে:

  • Statement আর branch coverage-এর পার্থক্য, উদাহরণসহ? — Statement প্রতিটি node, branch প্রতিটি decision-এর T ও F edge; else-হীন if-এ শুধু T নিলেই statement ১০০% কিন্তু branch ৫০%.
  • V(G)-র তিনটি সূত্র কী, কখন খাটে? — E − N + 2P সব সময়; decision + ১ single-entry/exit আর দুই-মুখী decision-এ; region-সংখ্যা planar graph-এ, বাইরের এলাকাসহ।
  • Condition coverage কি decision coverage নিশ্চিত করে? — না; a && b-এ (T,F), (F,T) দুটিতেই decision মিথ্যা।
  • MC/DC কী, কয়টি test লাগে? — প্রতিটি condition বাকিগুলো স্থির রেখে একা ফল বদলাতে পারে, তা দেখানো; n condition-এ সাধারণত n + ১টি; DO-178C Level A-তে চাওয়া হয়।
  • Equivalent mutant কী, score-এ কী প্রভাব? — আচরণ আসলের সমান, কোনো test মারতে পারে না; score = killed ÷ (মোট − equivalent), তাই হর থেকে বাদ।

১০ · AI-এর যুগে — coverage বাড়ানো সহজ, test ভালো করা কঠিন

“এই ফাইলের coverage ১০০% করে দিন” — এমন অনুরোধে AI খুব দ্রুত কাজ করে, আর ঠিক এখানেই Goodhart-এর ফাঁদ সবচেয়ে গভীর। AI প্রায়ই এমন test লেখে যা function ডাকে কিন্তু কিছু যাচাই করে না, অথবা কোড পড়ে তার বর্তমান আচরণই প্রত্যাশা হিসেবে লিখে দেয় — implementation-এর প্রতিচ্ছবি। তখন bug-ও “প্রত্যাশিত” হয়ে যায়। আবার complexity-র হিসাব চাইলে AI আত্মবিশ্বাসের সাথে ভুল E বা N গোনে, অথবা coverage tool-এর এমন flag বলে, যা আসলে নেই।

AI-এর চেনা ভুলকীভাবে ধরবেন
Assertion ছাড়া বা দুর্বল assertion-এর test (assert result is not None)Mutation tool চালানো; অথবা হাতে একটি >= বদলে দেখা test fail করে কি না
প্রত্যাশিত মান কোড থেকে কপি, specification থেকে নয়প্রতিটি প্রত্যাশা requirement বা হাতের হিসাবের সাথে মেলানো
বানানো flag বা config key (যেমন অস্তিত্বহীন --branch-coverage)Tool-এর অফিসিয়াল documentation; একবার চালিয়ে দেখা
V(G) বা ন্যূনতম test-সংখ্যার ভুল হিসাবনিজে CFG আঁকা, দুই সূত্রে মেলানো; ছোট script-এ যাচাই
Coverage বাড়াতে private function সরাসরি test, ভঙ্গুর mock-এর স্তূপPublic আচরণ দিয়ে test (পর্ব ৪৬, Unit test); mock কম (পর্ব ৪৯)
Prompt-এ পুরো কোড, সাথে secret বা গ্রাহকের তথ্যআগে গোপন মান মুছে ফেলা; অনুমোদিত tool-ই ব্যবহার

AI-কে test-এর সহকারী হিসেবে ব্যবহারের একটি prompt — এতে coverage-এর বদলে mutation-কে লক্ষ্য করা আর প্রত্যাশা specification থেকে নেওয়ার নির্দেশ আছে:

তুমি একজন সতর্ক test engineer. নিচে একটি function আর তার specification দিলাম (secret মুছে দিয়েছি)। [function-এর কোড] [specification: নিয়মগুলো বাংলায়] ১. প্রথমে function-টির control-flow graph-এর node আর edge তালিকা দাও, তারপর V(G) দুইভাবে হিসাব করো। ২. প্রতিটি test-এর প্রত্যাশিত ফল specification থেকে নেবে, কোড থেকে নয়। কোড আর specification না মিললে আলাদা করে জানাবে। ৩. প্রতিটি test-এ নির্দিষ্ট মানের assert থাকবে; শুধু "চলে কি না" যাচাই করবে না। ৪. সীমার মান (যেমন ঠিক ৪০, ৩৯, ১০০, ১০১, শূন্য, খালি তালিকা) আলাদা test-এ রাখো। ৫. শেষে বলো কোন mutant (যেমন >= থেকে >, + থেকে -) তোমার কোন test মারবে। কোনো tool-এর flag নিশ্চিত না হলে "যাচাই দরকার" লিখবে।

AI-এর সাহায্যে test আর metric — চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।

প্র ০১২০১৪ সালে Apple-এর TLS কোডে “goto fail” নামে পরিচিত একটি bug-এ একটি লাইন দুবার লেখা ছিল। Coverage-এর কোন মাপ এটি ধরতে পারত, আর কেন সেটিই যথেষ্ট হতো না?

২০১৪ সালের ফেব্রুয়ারিতে Apple iOS আর OS X-এর একটি নিরাপত্তা হালনাগাদ প্রকাশ করে (CVE-2014-1266). পরে প্রকাশিত open-source কোডে দেখা যায়, SecureTransport library-র server-এর key exchange-এর স্বাক্ষর যাচাই করার একটি function-এ পরপর দুটি if (...) goto fail; লাইনের পরে goto fail; লাইনটি আরেকবার লেখা ছিল — কোনো শর্ত ছাড়া। C-তে if-এর পরে বন্ধনী না থাকলে শুধু পরের একটি statement শর্তের অধীনে থাকে; তাই দ্বিতীয় goto fail; সব সময় চলত। fail লেবেলে তখন error-এর ভেরিয়েবলে ছিল সফলতার মান, তাই function “সফল” ফেরত দিত, আর স্বাক্ষর যাচাইয়ের শেষ ধাপ কখনো চলত না। ফলে নির্দিষ্ট পরিস্থিতিতে আক্রমণকারী নকল server-কে আসল হিসেবে চালিয়ে দিতে পারত বলে নিরাপত্তা-বিশ্লেষকেরা ব্যাখ্যা করেন।

Coverage-এর দিক থেকে এটি শিক্ষণীয়। দ্বিতীয় goto fail;-এর পরের লাইনগুলো হয়ে গিয়েছিল dead code — কোনো ইনপুটেই সেখানে পৌঁছানো যেত না। তাই যেকোনো test suite-এ statement coverage ১০০%-এর নিচে থাকত, আর coverage রিপোর্টের লাল লাইন প্রশ্ন তুলত: “স্বাক্ষর যাচাইয়ের লাইনটি কেন কখনো চলে না?” Compiler-এর unreachable code-এর সতর্কতাও (যেমন Clang-এর -Wunreachable-code, যা সাধারণ সতর্কতার দলে default-ভাবে চালু থাকে না) একই জায়গা দেখাত। Code review-এ indentation দেখে চোখ ধোঁকা খেয়েছিল — দুটি লাইন সমান ভেতরে, যেন দুটিই শর্তের অধীনে।

কিন্তু coverage একা যথেষ্ট হতো না। কারও রিপোর্টটি পড়ে প্রশ্নটি করতে হতো; অনেক বড় codebase-এ ৮৫% coverage “ঠিক আছে” বলে মেনে নেওয়া হয়, আর বাকি ১৫%-এর কোন লাইন গুরুত্বপূর্ণ, কেউ দেখে না। আসল রক্ষাকবচ ছিল একটি negative test: ভুল স্বাক্ষরসহ একটি handshake দিয়ে দেখা যে যাচাই ব্যর্থ হয়। এই test-টি থাকলে bug প্রথম দিনেই ধরা পড়ত — coverage যত ছিল তত থাকলেও। প্রচলিত শিক্ষাগুলো তাই কয়েকটি: if-এ সব সময় বন্ধনী, unreachable code-এর সতর্কতা চালু, নিরাপত্তার কোডে খারাপ ইনপুটের test বাধ্যতামূলক, আর coverage রিপোর্টে “না-চলা লাইন” আলাদা করে পড়া।

মূল উপলব্ধি: Coverage-এর সবচেয়ে মূল্যবান তথ্য সংখ্যা নয়, কোন লাইনগুলো চলেনি তার তালিকা। আর কোন আচরণ যাচাই করা দরকার — “ভুল স্বাক্ষর প্রত্যাখ্যাত হবে” — তা আসে specification থেকে, coverage থেকে নয়।

প্র ০২বিমানের সবচেয়ে গুরুত্বপূর্ণ software-এ MC/DC চাওয়া হয়, অথচ সাধারণ ওয়েব অ্যাপে কেউ চায় না। এই পার্থক্য কি যুক্তিসংগত? MC/DC-র নিজের দুর্বলতা কী?

পার্থক্যের মূলে ঝুঁকি আর খরচের হিসাব। DO-178B (১৯৯২) আর DO-178C (২০১১) software-কে ব্যর্থতার পরিণতি অনুযায়ী স্তরে ভাগ করে: Level A-তে ব্যর্থতা বিপর্যয় ঘটাতে পারে, যেমন flight control. সেখানে একটি শর্তও যদি কখনো আলাদাভাবে যাচাই না হয়, তবে সেই শর্তের ভুল — উল্টো লেখা, ভুল সংকেতের নাম — হাজার ঘণ্টার উড্ডয়নের পরে কোনো বিরল অবস্থায় প্রকাশ পেতে পারে। MC/DC প্রতিটি condition-এর জন্য একটি independence pair চায়, তাই এমন “প্রভাবহীন” শর্ত লুকিয়ে থাকতে পারে না। আবার multiple-condition coverage-এর 2ⁿ খরচও লাগে না — n + ১-এর কাছাকাছি test-এ হয়। এই ভারসাম্যের জন্যই John Chilenski ও Steven Miller-এর ১৯৯৪ সালের বহুল উদ্ধৃত প্রবন্ধসহ নানা কাজে MC/DC-কে যুক্তিসংগত মধ্যপথ বলা হয়েছে।

একটি ওয়েব অ্যাপে ব্যর্থতার পরিণতি সাধারণত ভিন্ন — একটি বাটন কাজ না করলে দ্রুত ঠিক করে আবার deploy করা যায়, আর ক্ষতি সীমিত। সেখানে MC/DC-র খরচ (প্রতিটি যুক্ত শর্তের জোড়া খোঁজা, tool, প্রমাণপত্র) লাভের চেয়ে বেশি। তবে ওয়েব অ্যাপেরও কিছু অংশ — পেমেন্টের নিয়ম, অনুমতি যাচাই — বেশি কড়া যাচাই দাবি করে; সেখানে শর্ত-স্তরের চিন্তা কাজে লাগে।

MC/DC-র নিজের দুর্বলতাও আছে। এটি শুধু একটি decision-এর ভেতরের গঠন দেখে; কোডকে কয়েকটি ছোট decision-এ ভেঙে লিখলে (মাঝে একটি boolean variable রেখে) MC/DC-র দাবি কমে যায়, অথচ যুক্তি একই — গবেষকেরা একে কোডের গঠনের ওপর নির্ভরশীলতা বলে সমালোচনা করেছেন। Short-circuit ভাষায় কোন রূপ (unique-cause না masking) গ্রহণযোগ্য, তা নিয়েও ব্যাখ্যার পার্থক্য আছে। আর MC/DC-ও বলে না প্রত্যাশিত ফল সঠিক কি না — সেটি আসে requirement-ভিত্তিক test থেকে; DO-178C-তে structural coverage-কে মূলত requirement-ভিত্তিক test কতটা সম্পূর্ণ তার পরীক্ষা হিসেবেই দেখা হয়।

মূল উপলব্ধি: কোন coverage মাপকাঠি “ঠিক”, তা ঝুঁকি ঠিক করে। কড়া মাপকাঠি তখনই যুক্তিসংগত যখন একটি না-ধরা ভুলের দাম খুব বেশি — আর তখনও তা requirement-ভিত্তিক test-এর পরিপূরক, বিকল্প নয়।

প্র ০৩একজন manager ঘোষণা দিলেন: “এই quarter-এ সব repository-র coverage ৯০% না হলে release বন্ধ।” গবেষণা আর অভিজ্ঞতা থেকে এর কী প্রভাব আশা করবেন? বিকল্প কী?

প্রথম প্রশ্ন: coverage বাড়লে কি test-এর ভুল ধরার ক্ষমতা বাড়ে? Laura Inozemtseva আর Reid Holmes-এর ICSE ২০১৪-র প্রবন্ধ “Coverage Is Not Strongly Correlated with Test Suite Effectiveness” কয়েকটি বড় Java project-এ হাজারো এলোমেলো test suite বানিয়ে mutant মারার হার মাপে। আমাদের বোঝামতে তাঁদের মূল ফল: suite-এর আকার (test-এর সংখ্যা) নিয়ন্ত্রণ করলে coverage আর কার্যকারিতার সম্পর্ক দুর্বল থেকে মাঝারি হয়ে যায়; অর্থাৎ বেশি coverage-এর বড় অংশ শুধু “বেশি test” থেকে আসে। পরের কিছু গবেষণা ভিন্ন সেটিং-এ কিছুটা জোরালো সম্পর্ক পেয়েছে — বিষয়টি বিতর্কিত, তবে “coverage = মান” সমীকরণ কোনো গবেষণা সমর্থন করে না।

দ্বিতীয় প্রশ্ন: সংখ্যা লক্ষ্য হলে মানুষ কী করবেন? Goodhart-এর সূত্র অনুযায়ী, যা মাপা হয় তা-ই অপ্টিমাইজ হয়। Deadline-এর চাপে প্রথমে আসে assertion-হীন বা দুর্বল-assertion test, getter-setter-এর অর্থহীন test, আর AI দিয়ে বানানো বিপুল test — যা coverage বাড়ায়, কিন্তু refactoring-এর সময় ভাঙে আর কিছু ধরে না। পুরোনো legacy project-এ ৯০% পৌঁছাতে দলগুলো নতুন feature বন্ধ রাখবে, অথবা কিছু ফাইল coverage-এর হিসাব থেকে বাদ দেওয়ার config লিখবে। ফল: সংখ্যা সবুজ, আস্থা কম।

বিকল্প কী? (১) নতুন বা বদলানো কোডের coverage দেখা — পুরোনো কোডের দায় একসাথে না চাপিয়ে। (২) সংখ্যা কমতে না দেওয়ার নিয়ম, নির্দিষ্ট শতাংশের বদলে। (৩) ঝুঁকিপূর্ণ module-এ (টাকা, অনুমতি) branch coverage আর বদলানো লাইনে mutation testing. (৪) Review-এ প্রশ্ন: “এই নতুন শাখা কোন test-এ চলে, আর ভুল হলে কোন assertion fail করবে?” (৫) Release-এর পরে পাওয়া bug-এর বিশ্লেষণ — test কেন ধরেনি — সবচেয়ে সৎ মাপ। একটি সংখ্যা চাইলে, mutation score মোট coverage-এর চেয়ে test-এর শক্তির কাছাকাছি মাপ, যদিও সেটিও খেলা যায়।

মূল উপলব্ধি: Coverage একটি টর্চলাইট — অন্ধকার কোণ দেখায়। একে release-এর তালা বানালে মানুষ টর্চ জ্বালিয়ে রাখা শেখেন, কোণগুলো দেখা নয়।

প্র ০৪Cyclomatic complexity প্রায় পঞ্চাশ বছর ধরে ব্যবহৃত, আবার বহু সমালোচিতও। এটি আসলে কী মাপে, কোথায় বিভ্রান্ত করে, আর কীভাবে বুদ্ধিমানের মতো ব্যবহার করবেন?

McCabe-এর ১৯৭৬ সালের প্রবন্ধের লক্ষ্য ছিল দুটি: module-এর জটিলতার একটি গাণিতিক মাপ, আর test-এর জন্য একটি ভিত্তি। দ্বিতীয়টিতে এটি চমৎকার — V(G) সত্যিই বলে একটি basis set-এ কতগুলো পথ, আর সেই পথ ধরে test নকশা করা যায়। প্রথম দাবি নিয়েই বিতর্ক। Martin Shepperd-এর ১৯৮৮ সালের সমালোচনামূলক প্রবন্ধসহ বেশ কিছু কাজ দেখিয়েছে, অনেক data-set-এ V(G) লাইন-সংখ্যার সাথে খুব জোরালোভাবে সম্পর্কিত — অর্থাৎ অনেক ক্ষেত্রে এটি প্রায় “আকার”-ই মাপে, আর ত্রুটির পূর্বাভাসে লাইন-সংখ্যার চেয়ে খুব বেশি বাড়তি তথ্য দেয় না।

কোথায় বিভ্রান্ত করে? প্রথমত, switch: ২০টি case-এর একটি সরল সারণি-ধাঁচের switch পড়া খুব সহজ, অথচ V(G) ২১. দ্বিতীয়ত, nesting: তিনটি পরপর if আর তিন স্তর ভেতরে বসানো তিনটি if — দুটিরই V(G) ৪, অথচ দ্বিতীয়টি পড়া অনেক কঠিন। এই দুই ফাঁকের জন্যই SonarSource-এর cognitive complexity এসেছে। তৃতীয়ত, ভাষা আর tool-ভেদে গোনার নিয়ম আলাদা — &&, ternary, exception — তাই দুই project-এর সংখ্যা তুলনা প্রায় অর্থহীন। চতুর্থত, function ভেঙে সংখ্যা কমানো সহজ: একটি ৩০-এর function দশটি ৪-এর function-এ ভাঙলে প্রতিটি “সবুজ”, কিন্তু পুরো আচরণ বুঝতে এখন দশটি জায়গায় লাফাতে হয়।

তাহলে বুদ্ধিমান ব্যবহার কী? (১) Test-এর হাতিয়ার হিসেবে: একটি function-এর V(G) ১২ হলে, ১২টি স্বাধীন পথের কয়টি আপনার test ছোঁয়? (২) প্রবণতা আর hotspot: যে function-এর complexity বাড়ছে আর যেটি ঘনঘন বদলাচ্ছে, সেটি refactoring-এর প্রথম প্রার্থী — Adam Tornhill-এর মতো লেখকেরা version control-এর ইতিহাস আর complexity মিলিয়ে এমন বিশ্লেষণ জনপ্রিয় করেছেন। (৩) Review-এর সংকেত হিসেবে, কঠিন নিয়ম হিসেবে নয়: সীমা ছাড়ালে প্রশ্ন, স্বয়ংক্রিয় প্রত্যাখ্যান নয়। (৪) Cognitive complexity আর nesting depth পাশাপাশি দেখা।

মূল উপলব্ধি: V(G) test-এর জন্য একটি সুনির্দিষ্ট, প্রমাণযোগ্য সংখ্যা; বোঝার কষ্টের জন্য একটি মোটা সংকেত মাত্র। কোন প্রশ্নের উত্তর চান, তা ঠিক করে metric বাছুন।

প্র ০৫Mutation testing-এর ধারণা ১৯৭০-এর দশকের, অথচ শিল্পে ব্যবহার বেড়েছে অনেক পরে। এত দেরি কেন, আর বড় প্রতিষ্ঠান কীভাবে এটিকে ব্যবহারযোগ্য করেছে বলে জানা যায়?

DeMillo, Lipton আর Sayward-এর ১৯৭৮ সালের প্রবন্ধে ধারণাটি স্পষ্ট ছিল, কিন্তু তিনটি বাধা দীর্ঘদিন একে গবেষণাগারে আটকে রেখেছিল। প্রথম বাধা খরচ: প্রতিটি mutant-এর জন্য পুরো program আবার compile আর test suite আবার চালানো — সেই যুগের কম্পিউটারে বড় program-এ এটি দিন বা সপ্তাহের কাজ। দ্বিতীয় বাধা equivalent mutant: কোন mutant-এর আচরণ আসলের সমান, তা সাধারণভাবে নির্ধারণ করা যায় না (এটি program-এর সমতা যাচাইয়ের মতো undecidable সমস্যা)। তাই survived mutant-এর তালিকার একটি অংশ মানুষকে হাতে পরীক্ষা করে বাদ দিতে হয় — ক্লান্তিকর আর ব্যয়বহুল। তৃতীয় বাধা ব্যবহারিক tool-এর অভাব: দশকের পর দশক tool ছিল মূলত গবেষণার জন্য।

কী বদলাল? দ্রুত কম্পিউটার, test-এর ব্যাপক স্বয়ংক্রিয়তা (CI), bytecode-স্তরে mutant বানানো (যেমন Java-র PIT, যা আবার compile না করেই mutant তৈরি করে), আর coverage তথ্য দিয়ে প্রতিটি mutant-এ শুধু প্রাসঙ্গিক test চালানো। সবচেয়ে প্রভাবশালী পরিবর্তন সম্ভবত দৃষ্টিভঙ্গির: পুরো codebase-এর score-এর বদলে review-এর মুহূর্তে, শুধু বদলানো লাইনে কয়েকটি mutant দেখানো। Goran Petrović আর Marko Ivanković-এর “State of Mutation Testing at Google” (ICSE SEIP ২০১৮) এমন একটি ব্যবস্থার বিবরণ দেয় বলে জানা যায়: diff-এর লাইনে বাছাই করা mutant, অপ্রয়োজনীয় বা নিরর্থক mutant (যেমন logging-এর লাইন) বাদ দেওয়ার নিয়ম, আর survived mutant code review-এর মন্তব্য হিসেবে দেখানো, যাতে লেখক সিদ্ধান্ত নেন test যোগ করবেন কি না। প্রকাশিত বিবরণে ডেভেলপাররা এই মন্তব্যের বড় অংশকে কাজের বলে সাড়া দিয়েছিলেন বলে জানানো হয়।

ছোট দলের জন্য শিক্ষাটি একই: পুরো project-এ মাসে একবার mutation score মাপার চেয়ে, ঝুঁকিপূর্ণ module আর নতুন কোডে নিয়মিত অল্প mutant অনেক বেশি কাজের। Survived mutant-কে “score কম” হিসেবে না দেখে প্রশ্ন হিসেবে দেখুন: “এই >=-কে > করলে কোনো test fail করে না — সীমার test কোথায়?”

মূল উপলব্ধি: একটি ভালো ধারণা শিল্পে আসে তখনই, যখন তার খরচ মানুষের কাজের প্রবাহের সাথে মেলে। Mutation testing সফল হয়েছে ছোট, সময়মতো আর নির্দিষ্ট প্রশ্ন হয়ে — বড় রিপোর্ট হয়ে নয়।

অনুশীলন

প্রথমে খাতায় CFG এঁকে নিজে উত্তর লিখুন, তারপর মিলিয়ে দেখুন।

  1. হিসাব — V(G): (ক) নিচের কাল্পনিক পাঠাগারের বিলম্ব-ফি function-এর condition-স্তরের CFG আঁকুন (যুক্ত শর্ত ভেঙে), N আর E গুনুন, আর V(G) তিনভাবে বের করুন। Decision-স্তরে (যুক্ত শর্ত একটি node) V(G) কত হতো? (খ) একটি program-এ তিনটি আলাদা function-এর CFG একসাথে ধরলে N = ২০, E = ২৪. V(G) কত?
    function lateFee(days, member) {
      let fee = 0;
      if (days > 14) {
        fee = (days - 14) * 2;
      }
      if (member && fee > 0) {
        fee = fee / 2;
      }
      return fee;
    }

    (ক) Node: ১ = fee = 0, ২ = days > 14, ৩ = fee = (days - 14) * 2, ৪ = member, ৫ = fee > 0, ৬ = fee = fee / 2, ৭ = return fee. Edge: ১→২, ২→৩ (T), ২→৪ (F), ৩→৪, ৪→৫ (T), ৪→৭ (F, short-circuit), ৫→৬ (T), ৫→৭ (F), ৬→৭ — মোট ৯. তাই N = ৭, E = ৯.

    • E − N + 2P = ৯ − ৭ + ২ = ৪
    • Decision node: ২, ৪, ৫ — তিনটি, তাই ৩ + ১ = ৪
    • Region: ত্রিভুজ ২-৩-৪, ৪-৫-৭-র এলাকা, ৫-৬-৭-এর এলাকা — তিনটি বদ্ধ, সাথে বাইরের একটি = ৪

    Decision-স্তরে member && fee > 0 একটি node: N = ৬, E = ৭ (১→২, ২→৩, ২→D, ৩→D, D→৬, D→৭, ৬→৭), V(G) = ৭ − ৬ + ২ = ৩ = ২টি decision + ১. পার্থক্যটি ঠিক যুক্ত শর্তের একটি বাড়তি condition. (খ) V(G) = E − N + 2P = ২৪ − ২০ + ২ × ৩ = ১০. আলাদা function-গুলোর V(G) যোগ করলেও একই আসে — প্রতিটি function-এর E − N + ২ যোগ করলে মোট E − N + ৬.

  2. হিসাব — ন্যূনতম test: অনুশীলন ১-এর lateFee-এর জন্য প্রতিটি মাপকাঠিতে সবচেয়ে কম কয়টি test লাগবে, আর কোন ইনপুট? (ক) statement, (খ) branch, (গ) member && fee > 0-এর MC/DC, (ঘ) একই decision-এর multiple-condition, (ঙ) decision-স্তরে path coverage — graph-এ কয়টি পথ, আর বাস্তবে কয়টি চালানো যায়?
    • (ক) ১টি: (20, true) — fee = ১২, সদস্য, ১২ > ০, তাই fee = ৬; সব লাইন চলে।
    • (খ) ২টি: (20, true) দুই decision-এর T, আর (5, false) দুটির F.
    • (গ) ৩টি: (member, fee > 0) = (T,T), (F,T), (T,F) → (20, true), (20, false), (5, true). (T,T) আর (F,T) জোড়া member-এর প্রভাব দেখায়, (T,T) আর (T,F) জোড়া fee > 0-এর। একই তিনটি test প্রথম decision-এর দুই দিকও ছোঁয় (২০ আর ৫), তাই পুরো function-এর MC/DC-ও ৩টিতে।
    • (ঘ) ৪টি: ওপরের তিনটি আর (F,F) = (5, false). (Short-circuit-এ member মিথ্যা হলে fee > 0 মূল্যায়নই হয় না — তাই যন্ত্রের চোখে (F,T) আর (F,F) একই পথ; মাপকাঠিটি তবু মানের সমন্বয় গোনে।)
    • (ঙ) Graph-এ পথ ৪টি (দুই decision-এর ২ × ২)। কিন্তু “প্রথম decision F, দ্বিতীয় T” — দিন ১৪ বা কম হলে fee = ০, তাই fee > 0 মিথ্যা, decision কখনো সত্য হবে না: infeasible path. চালানো যায় ৩টি. (একটি ছোট script-এ ০–৩৯ দিন আর দুই রকম সদস্যপদ চালিয়ে মিলিয়েছি: শুধু ৩টি পথ দেখা গেছে, যদিও দুই condition-এর চারটি সমন্বয়ই পাওয়া যায়।)
  3. আউটপুট অনুমান করুন: অংশ ৯-এর mutation_lab.js-এ শেষ দুই লাইনের বদলে শুধু এটি চালানো হলো — দুর্বল suite-এর সাথে একটিমাত্র test যোগ করে। প্রোগ্রাম কী ছাপবে, আর কোন mutant এখনো বেঁচে আছে কেন?
    run("weak+40", [...WEAK, [40, 0, false, true]]);

    আসলে চালালে আউটপুট:

    --- weak+40 suite (3 tests) ---
    M1 relational  >= to >    killed by (40,0,false)
    M2 constant    40 to 41   killed by (40,0,false)
    M3 arithmetic  + to -     SURVIVED
    M4 logical     negate if  killed by (75,0,false)
    M5 constant    100 to 101 survived (equivalent)
    mutation score = 3/(5-1) = 75%

    ৪০-এর test একসাথে দুটি mutant মারে: M1 (> দিলে ৪০ ফেল) আর M2 (৪১ দিলে ৪০ ফেল)। M4 আগের মতোই ৭৫-এর test-এ মরে। M3 (+ → -) বেঁচে আছে, কারণ তিনটি test-এই বোনাস ০ — marks + 0 আর marks - 0 সমান। একে মারতে বোনাস শূন্যের বেশি আর সীমার কাছে এমন test লাগবে, যেমন (38, 2, false) → true. M5 equivalent, তাই হর ৪: score = ৩ ÷ ৪ = ৭৫%. লক্ষ করুন, এই তিন test-এর branch coverage আগের দুইটির মতোই — ৪০ যোগ করে coverage বাড়েনি, mutation score বেড়েছে ২৫% থেকে ৭৫%.

  4. নকশা ও সমালোচনা: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালের দল নিচের নিয়ম প্রস্তাব করল। প্রতিটির দুর্বলতা বলুন, আর একটি ভালো বিকল্প নীতি লিখুন (৫–৬টি নিয়ম)। (১) “প্রতিটি PR-এ মোট line coverage ৯৫% বা বেশি, নইলে merge বন্ধ।” (২) “Cyclomatic complexity ১০-এর বেশি হলে CI লাল — কোনো ব্যতিক্রম নয়।” (৩) “AI দিয়ে test লিখিয়ে coverage দ্রুত বাড়ানো হবে।” (৪) “Mutation testing খুব ধীর, তাই কখনো নয়।”
    • (১) Line coverage branch-এর ফাঁক দেখায় না; মোট সংখ্যা legacy কোডের দায় প্রতিটি PR-এ চাপায়; ৯৫% লক্ষ্য assertion-হীন test আর ভঙ্গুর mock-কে উৎসাহ দেয় (Goodhart).
    • (২) Tool-ভেদে গোনা আলাদা; সরল বড় switch বা একটি পার্সারের state-সারণি অযথা আটকাবে; মানুষ function টুকরো করে সংখ্যা কমাবেন, বোঝা সহজ না করেই। সীমা হোক সতর্কসংকেত, ব্যতিক্রম লিখিত কারণসহ।
    • (৩) AI প্রায়ই কোডের বর্তমান আচরণ প্রত্যাশা হিসেবে কপি করে — bug-ও “প্রত্যাশিত” হয়; assertion দুর্বল হতে পারে। AI-লেখা test-এ mutation বা হাতে-বসানো ভুল দিয়ে যাচাই বাধ্যতামূলক।
    • (৪) পুরো codebase-এ ধীর, কিন্তু বদলানো লাইন আর ঝুঁকিপূর্ণ module-এ (নম্বর থেকে গ্রেড, GPA-র হিসাব) অল্প mutant দ্রুত আর খুব কাজের।

    একটি বিকল্প নীতি:

    1. Coverage মাপা হবে branch mode-এ; gate থাকবে নতুন বা বদলানো কোডে (যেমন ৮০%), আর মোট সংখ্যা কমতে দেওয়া হবে না।
    2. গ্রেড আর GPA-র module-এ বদলানো লাইনে mutation testing; survived mutant review-এ মন্তব্য হিসেবে আসবে।
    3. প্রতিটি test-এ নির্দিষ্ট মানের assertion; সীমার মান (৩৯, ৪০, ১০০) আলাদা test-এ।
    4. Cyclomatic আর cognitive complexity-র সীমা config-এ; ছাড়ালে review-এ আলোচনা, আর ব্যতিক্রম হলে কারণ লেখা।
    5. AI-লেখা test মানুষের লেখা test-এর মতোই review; প্রত্যাশা specification থেকে।
    6. প্রতি release-এর পরে পাওয়া bug-এর জন্য প্রশ্ন: কোন test ধরতে পারত, কেন ধরেনি — উত্তর থেকে নিয়ম হালনাগাদ।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমBasis path testing-এর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✏️ কোড থেকে control-flow graph আঁকা
  2. 🔢 V(G) হিসাব, দুই সূত্রে মিলিয়ে
  3. 🧭 V(G)টি স্বাধীন পথের basis set বের করা
  4. 🎯 প্রতিটি পথ চালাবে এমন ইনপুট বাছা
  5. 📋 Specification থেকে প্রত্যাশিত ফল লেখা
  6. ▶️ Test চালিয়ে আসল ফল মেলানো

একটি function-এর control-flow graph-এ ১৩টি edge আর ১১টি node আছে, আর graph-টি একটিমাত্র যুক্ত অংশ (P = 1). Cyclomatic complexity V(G) কত?

  • ২
  • ৪
  • ৬
  • ২৪

if (a && b) { x = 1; } — দুটি test: (a = true, b = false) আর (a = false, b = true). এই দুই test কোন মাপকাঠি পূরণ করে?

  • Statement আর condition coverage দুটিই
  • Branch coverage
  • শুধু condition coverage
  • MC/DC

if (a || b || c) — তিনটি স্বাধীন boolean condition. Unique-cause MC/DC আর multiple-condition coverage-এর জন্য সবচেয়ে কম কয়টি test লাগে?

  • MC/DC ৪, multiple-condition ৮
  • MC/DC ৩, multiple-condition ৬
  • MC/DC ৮, multiple-condition ৮
  • MC/DC ২, multiple-condition ৪

একটি module-এ mutation tool ২০টি mutant বানাল। Test suite ১৫টি মারল, ৫টি বাঁচল; বেঁচে থাকাদের ২টি পরীক্ষা করে equivalent পাওয়া গেল। Mutation score কত?

  • ৭৫%
  • ৮৫%
  • ৬০%
  • ≈ ৮৩.৩%

একটি দলের test suite-এ ১০০% branch coverage. নিচের কোন দাবিটি অবশ্যই সত্য?

  • কোডে আর কোনো bug নেই
  • ১০০% statement coverage-ও পূরণ হয়েছে
  • প্রতিটি সম্ভাব্য path চালানো হয়েছে
  • MC/DC-ও পূরণ হয়েছে

দুটি function-এ তিনটি করে দুই-মুখী if. প্রথমটিতে তিনটি if পরপর, দ্বিতীয়টিতে একটির ভেতরে আরেকটি, তিন স্তর পর্যন্ত। SonarSource-এর cognitive complexity আর McCabe-এর cyclomatic complexity সম্পর্কে কোনটি ঠিক?

  • দ্বিতীয়টির cyclomatic complexity বেশি
  • দুটি মাপেই দুই function সমান
  • Cyclomatic দুটিতেই ৪; cognitive প্রথমটিতে ৩, দ্বিতীয়টিতে ৬
  • Cognitive complexity প্রথমটিতে বেশি

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. CFG: node = basic block, edge = সম্ভাব্য পরের ধাপ, decision node থেকে T/F দুই তীর, loop মানে back edge. যুক্ত শর্ত ভাঙলে condition-স্তরের ছবি।
  2. সিঁড়ি: branch ⊃ statement; C/DC ⊃ condition আর decision; MC/DC ⊃ C/DC; MCC ⊃ MC/DC; path ⊃ basis path ⊃ branch. Condition coverage একা কিছু নিশ্চিত করে না। if (a && b)-এ ন্যূনতম: statement ১, branch ২, C/DC ২, MC/DC ৩, MCC ৪.
  3. V(G) = E − N + 2P = decision + ১ = region: স্বাধীন পথের সংখ্যা, basis path testing-এর test-সংখ্যা; McCabe-এর সীমা ১০ একটি রীতি, tool-ভেদে নিয়ম আলাদা। Cognitive complexity nesting-কে শাস্তি দেয়, switch-কে নয়।
  4. Coverage প্রয়োজনীয়, যথেষ্ট নয়: assertion-হীন test-ও ১০০% দেয়; diff coverage আর “কমতে দেব না” gate মোট সংখ্যার লক্ষ্যের চেয়ে নিরাপদ। Tool-এ branch mode চালু রাখুন।
  5. Mutation: killed, survived, equivalent; score = killed ÷ (মোট − equivalent). Boundary test ছাড়া relational আর constant mutant বাঁচে। খরচ কমাতে বদলানো কোডে আর বাছাই করা test-এ চালান।

পরের পর্বে: এ পর্যন্ত test ছিল একটি function-এর ভেতরে। কিন্তু আসল bug প্রায়ই লুকিয়ে থাকে অংশগুলোর জোড়ায় — database, অন্য service, browser. পর্ব ৫১-এ test pyramid, integration test, contract test, end-to-end test আর অনির্ভরযোগ্য (flaky) test.

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান