পাঠ ২৪ · ৫৭-এর মধ্যে · মডিউল ৬
Home / Courses / Software Testing & Quality Assurance / টেস্ট কভারেজ ও মেট্রিক্স

পাথ কভারেজ ও সাইক্লোমেটিক কমপ্লেক্সিটি

Path coverage & cyclomatic complexity
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • পাথ কভারেজ কী এবং এটি ব্রাঞ্চ কভারেজ থেকে কীভাবে আলাদা
  • সাইক্লোমেটিক কমপ্লেক্সিটির সূত্র এবং এর পেছনের যুক্তি
  • সোর্স কোড থেকে একটি সরলীকৃত কমপ্লেক্সিটি এস্টিমেটর নিজে বাস্তবায়ন করা
  • কমপ্লেক্সিটি সংখ্যাকে দরকারি টেস্ট কেসের সংখ্যার সাথে সম্পর্কিত করা

১ · পাথ কভারেজ: প্রতিটি রুট আলাদাভাবে গুরুত্বপূর্ণ

পাথ কভারেজPath Coverageএকটি ফাংশনের কন্ট্রোল-ফ্লো গ্রাফের মধ্য দিয়ে সম্ভাব্য প্রতিটি স্বতন্ত্র (independent) রুটে অন্তত একটি টেস্ট কেস চলে গেছে কি না তার পরিমাপ। L23-এ আমরা দেখেছি ব্রাঞ্চ কভারেজ প্রতিটি একক সিদ্ধান্তের উভয় দিক আলাদাভাবে যাচাই করে। কিন্তু যখন একটি ফাংশনে একাধিক সিদ্ধান্ত একসাথে থাকে, তখন সেগুলোর কম্বিনেশন — অর্থাৎ একটি নির্দিষ্ট ক্রমে একাধিক শর্ত মিলে যে "পথ" তৈরি হয় — সেটিও গুরুত্বপূর্ণ হয়ে ওঠে। দুটো if স্টেটমেন্ট থাকা একটি ফাংশনে প্রতিটি if-এর ব্রাঞ্চ আলাদাভাবে ১০০% কভার হয়ে গেলেও, "প্রথমটি True আর দ্বিতীয়টি False" নির্দিষ্ট কম্বিনেশনটি হয়তো কখনো একসাথে টেস্ট করা হয়নি — সেটিই একটি অ-কভার হওয়া পাথ।

একটি ফাংশনে সিদ্ধান্ত-বিন্দু যত বাড়ে, সম্ভাব্য পাথের সংখ্যা তত দ্রুত (সূচকীয় হারে) বেড়ে যায় — তাই বাস্তবে "১০০% পাথ কভারেজ" প্রায়ই অবাস্তব লক্ষ্য হয়ে দাঁড়ায়, বিশেষ করে লুপ থাকলে। এই কারণেই আমাদের একটি সংখ্যাগত ধারণা দরকার — একটি ফাংশন কতটা জটিল, অর্থাৎ তাতে কতগুলো স্বতন্ত্র পথ থাকতে পারে — যা দিয়ে আমরা বুঝতে পারি কোন ফাংশনগুলোতে সবচেয়ে বেশি টেস্ট-মনোযোগ দরকার। এই সংখ্যাগত ধারণাটিই সাইক্লোমেটিক কমপ্লেক্সিটি।

২ · সাইক্লোমেটিক কমপ্লেক্সিটি: সূত্র ও একটি কাজ-করা এস্টিমেটর

সাইক্লোমেটিক কমপ্লেক্সিটিCyclomatic Complexityএকটি ফাংশনের কন্ট্রোল-ফ্লো গ্রাফে থাকা স্বতন্ত্র (independent) লিনিয়ার পথের সংখ্যার একটি সুপরিচিত, প্রতিষ্ঠিত পরিমাপ — সাধারণত M = 1 + সিদ্ধান্ত-বিন্দুর সংখ্যা হিসেবে গণনা করা হয়। হলো ১৯৭৬ সালে টমাস ম্যাককেব প্রবর্তিত একটি সুপরিচিত, ব্যাপকভাবে-স্বীকৃত মেট্রিক। সূত্রটি সহজ: $$M = 1 + D$$ যেখানে $D$ হলো ফাংশনের ভেতরের মোট সিদ্ধান্ত-বিন্দুর (decision point) সংখ্যা — প্রতিটি if, elif, for, while, এবং প্রতিটি কম্পাউন্ড কন্ডিশনের and/or একটি করে নতুন স্বতন্ত্র পথের সম্ভাবনা তৈরি করে। বেসলাইন +1 ধরা হয় কারণ কোনো সিদ্ধান্ত-বিন্দু না থাকলেও ফাংশনের মধ্য দিয়ে অন্তত একটি পথ (উপর থেকে নিচে, সরলরৈখিকভাবে) সবসময় থাকে।

সত্যিকারের টুল (radon, McCabe checker) ফাংশনের AST (Abstract Syntax Tree) পার্স করে প্রতিটি সিদ্ধান্ত-বিন্দু নির্ভুলভাবে গোনে — কমেন্ট বা স্ট্রিং লিটারেলের ভেতরের টেক্সট কখনো ভুল করে গোনে না। এই sandbox-এ থার্ড-পার্টি প্যাকেজ নেই, তাই নিচে আমরা একটি সরলীকৃত approximation বানাচ্ছি — সোর্স কোডের টেক্সটে সরাসরি কিওয়ার্ড খুঁজে গোনা। এটি স্পষ্টভাবে বলে রাখা দরকার: এটি একটি teaching-grade সরলীকরণ, একটি পূর্ণাঙ্গ AST-ভিত্তিক টুল নয় — বাস্তব প্রজেক্টে সঠিক কমপ্লেক্সিটি মাপতে radon-এর মতো ডেডিকেটেড টুল ব্যবহার করা উচিত।

Python
import re

def estimate_cyclomatic_complexity(name, source):
    # সরলীকৃত approximation: সোর্স কোডে সিদ্ধান্ত-বিন্দুর কিওয়ার্ড খুঁজে গোনা।
    # সত্যিকারের টুল (radon, McCabe checker) AST পার্স করে নির্ভুলভাবে গোনে --
    # এখানে আমরা \b...\b (word-boundary) রেজেক্স দিয়ে ভুল ম্যাচ (যেমন "order"-এর
    # ভেতরের "or") এড়াচ্ছি, কিন্তু কমেন্ট/স্ট্রিং-এর ভেতরের কিওয়ার্ডও গুনে ফেলতে পারে --
    # তাই এটি একটি আনুমানিক সংখ্যা, চূড়ান্ত সত্য নয়। (নোট: একটি সাধারণ .py ফাইলে
    # inspect.getsource(func) দিয়ে সরাসরি সোর্স আনা যেত -- কিন্তু এই লেসন-স্যান্ডবক্সে কোড
    # exec() দিয়ে চলে, তাই কোনো real ফাইল না থাকায় inspect.getsource() কাজ করে না; তাই
    # ফাংশনের সোর্স এখানে সরাসরি স্ট্রিং হিসেবে দেওয়া হলো।)
    decision_keywords = ["if", "elif", "for", "while", "and", "or"]
    keyword_counts = {}
    decision_points = 0
    for kw in decision_keywords:
        count = len(re.findall(r"\b" + kw + r"\b", source))
        keyword_counts[kw] = count
        decision_points += count
    complexity = 1 + decision_points
    return complexity, keyword_counts


add_tax_source = '''def add_tax(price, tax_rate):
    return price * (1 + tax_rate)
'''

validate_order_source = '''def validate_order(order):
    if order is None:
        return False
    if order.get("total", 0) <= 0:
        return False
    items = order.get("items", [])
    for item in items:
        if item.get("qty", 0) <= 0 or item.get("price", 0) < 0:
            return False
    if order.get("total", 0) > 0 and len(items) > 0:
        return True
    return False
'''

for name, source in (("add_tax", add_tax_source), ("validate_order", validate_order_source)):
    complexity, breakdown = estimate_cyclomatic_complexity(name, source)
    print(f"{name}():")
    print(f"  কিওয়ার্ড ব্রেকডাউন: {breakdown}")
    print(f"  আনুমানিক সাইক্লোমেটিক কমপ্লেক্সিটি: {complexity}")
    print(f"  -> পূর্ণ পাথ কভারেজের জন্য মোটামুটি {complexity}টি স্বতন্ত্র টেস্ট কেস দরকার হতে পারে\n")

    
add_tax()-এ কোনো সিদ্ধান্ত-বিন্দু নেই ($D=0$), তাই কমপ্লেক্সিটি $1+0=1$ — মাত্র একটি সরলরৈখিক পথ, একটি টেস্ট কেসই যথেষ্ট। validate_order()-এ চারটি if, একটি for, একটি and, ও একটি or মিলিয়ে $D=7$, তাই কমপ্লেক্সিটি $1+7=8$ — অর্থাৎ পূর্ণ পাথ কভারেজ পেতে হলে মোটামুটিভাবে ৮টি স্বতন্ত্র, ভালোভাবে-বাছাই-করা টেস্ট কেস দরকার হতে পারে (প্রতিটি if/for/and/or-এর ভিন্ন ভিন্ন কম্বিনেশন কভার করার জন্য) — শুধু ৮ বার এলোমেলোভাবে ফাংশনটি কল করলেই পাথ কভারেজ নিশ্চিত হয় না, টেস্ট কেসগুলো ইচ্ছাকৃতভাবে ভিন্ন ভিন্ন কম্বিনেশন লক্ষ্য করে ডিজাইন করতে হয়।
মূল কথা · Key takeaway

সাইক্লোমেটিক কমপ্লেক্সিটি একটি ফাংশনের "কতটা টেস্ট-কঠিন" তার একটি দ্রুত সংখ্যাগত সংকেত — সংখ্যা যত বেশি, ফাংশনে তত বেশি স্বতন্ত্র পথ, এবং পূর্ণ পাথ কভারেজের জন্য তত বেশি টেস্ট কেস দরকার। অনেক টিম একটি থ্রেশহোল্ড (যেমন ১০ বা ১৫)-এর বেশি কমপ্লেক্সিটির ফাংশনকে রিফ্যাক্টর করার সংকেত হিসেবে ব্যবহার করে। মনে রাখবেন — উপরের এস্টিমেটর একটি শিক্ষামূলক সরলীকরণ; বাস্তব CI পাইপলাইনে radon বা অনুরূপ AST-ভিত্তিক টুল ব্যবহার করা হয়, যা কমেন্ট/স্ট্রিং-এর ভেতরের ভুল ম্যাচ এড়িয়ে নির্ভুল সংখ্যা দেয়।

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

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

প্র ০১ কেন সাইক্লোমেটিক কমপ্লেক্সিটির সূত্রে বেসলাইন হিসেবে +1 যোগ করা হয়?

কারণ কোনো সিদ্ধান্ত-বিন্দু (if/for/while) না থাকলেও একটি ফাংশনের মধ্য দিয়ে অন্তত একটি পথ সবসময় থাকে — উপর থেকে নিচ পর্যন্ত সরলরৈখিক এক্সিকিউশন। add_tax()-এর মতো একটি ফাংশনে কোনো শাখা না থাকলেও সেটিকে চালাতে অন্তত একটি টেস্ট কেস তো লাগবেই — তাই ন্যূনতম কমপ্লেক্সিটি কখনো শূন্য হতে পারে না, সবসময় অন্তত ১।

প্র ০২ উপরের এস্টিমেটরটি কেন শুধুই একটি "আনুমানিক" সংখ্যা দেয়, একটি নির্ভুল সংখ্যা নয় কেন?

কারণ এটি সোর্স কোডের কাঁচা টেক্সটে কিওয়ার্ড খুঁজে গোনে, একটি প্রকৃত পার্সার নয়। যদি কোনো কমেন্টে লেখা থাকে # check if and or condition, অথবা কোনো স্ট্রিং লিটারেলে "for testing"-এর মতো শব্দ থাকে, তাহলে এই টেক্সট-ভিত্তিক পদ্ধতি ভুলবশত সেগুলোকেও সিদ্ধান্ত-বিন্দু হিসেবে গুনে ফেলতে পারে। একটি সত্যিকারের AST-ভিত্তিক টুল কোডকে সিনট্যাক্স ট্রি হিসেবে পার্স করে বোঝে কোনটি প্রকৃত কন্ট্রোল-ফ্লো কিওয়ার্ড আর কোনটি নিছক টেক্সট — তাই সবসময় নির্ভুল থাকে।

প্র ০৩ validate_order()-এর কমপ্লেক্সিটি ৮ হলে, এর মানে কি ঠিক ৮টি টেস্ট কেস লিখলেই ফাংশনটি "সম্পূর্ণ টেস্ট করা" হয়ে যাবে?

না। কমপ্লেক্সিটি ৮ শুধু বলে দেয় পূর্ণ পাথ কভারেজের জন্য ন্যূনতম কতগুলো স্বতন্ত্র টেস্ট কেস দরকার হতে পারে — এটি একটি নিম্নসীমা (lower bound), গ্যারান্টি নয়। আসল ৮টি টেস্ট কেস যদি ইচ্ছাকৃতভাবে ৮টি ভিন্ন কম্বিনেশন (প্রতিটি if-এর True/False, লুপে ০/১/একাধিক আইটেম, ইত্যাদি) লক্ষ্য করে ডিজাইন করা না হয় — যেমন যদি একই ধরনের ইনপুট বারবার দেওয়া হয় — তাহলে ৮ বার কল করলেও অনেক পথ অ-কভার থেকে যেতে পারে। সংখ্যাটি একটি গাইড, ডিজাইন এখনও ম্যানুয়াল চিন্তার কাজ।

অনুশীলন

  1. চিন্তা করুন: একটি ফাংশনে একটি while লুপের ভেতরে একটি if...elif...else আছে (একটি if, একটি elif)। এই ফাংশনের আনুমানিক সাইক্লোমেটিক কমপ্লেক্সিটি কত হবে?

    সিদ্ধান্ত-বিন্দু: ১টি while + ১টি if + ১টি elif = ৩টি (else নিজে কোনো নতুন সিদ্ধান্ত-বিন্দু নয়, কারণ এটি স্বতন্ত্র কোনো নতুন শর্ত পরীক্ষা করে না — শুধু আগের সব শর্ত False হলে যা বাকি থাকে তা করে)। তাই কমপ্লেক্সিটি $M = 1 + 3 = 4$।

  2. পরীক্ষা করুন: কোড সেলে validate_order-এর ভেতরের or-কে and-এ পরিবর্তন করে Run চাপুন। কমপ্লেক্সিটি সংখ্যা বদলায় কি না লক্ষ্য করুন — কেন বদলায় বা বদলায় না তা ব্যাখ্যা করুন।

    কমপ্লেক্সিটি সংখ্যা বদলাবে না — কারণ এস্টিমেটরটি শুধু কতগুলো and/or কিওয়ার্ড আছে তা গোনে, কোন নির্দিষ্ট কিওয়ার্ড ব্যবহার হয়েছে তা নয়। or-কে and-এ পরিবর্তন করলে কিওয়ার্ডের সংখ্যা একই থাকে (এখনও একটিই বুলিয়ান অপারেটর), শুধু ফাংশনের আচরণ বদলে যায় (কখন False রিটার্ন হবে তার যুক্তি পাল্টে যায়) — যা এই সরলীকৃত মেট্রিক ধরতে পারে না, আরেকটি সীমাবদ্ধতার উদাহরণ।

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

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • স্টেটমেন্ট ও ব্রাঞ্চ কভারেজ L23 sys.settrace() দিয়ে বানানো লাইন-কভারেজ ট্র্যাকারের সাথে এই পাঠের কমপ্লেক্সিটি এস্টিমেটর একসাথে ব্যবহার করে দেখতে পারেন কোন ফাংশন কতটুকু ভালোভাবে টেস্ট করা হয়েছে।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics, Full-Stack Web Frameworks, Mobile App Development, Ethics in Computing & AI Safety ও Software Testing & Quality Assurance — সব এক জায়গায়।
আগের পাঠ
স্টেটমেন্ট ও ব্রাঞ্চ কভারেজ