স্টেটমেন্ট ও ব্রাঞ্চ কভারেজ
এই পাঠে যা শিখবেন
- স্টেটমেন্ট কভারেজের সঠিক সংজ্ঞা এবং এটি কীভাবে গণনা করা হয়
sys.settrace()দিয়ে একটি কাজ-করা লাইন-কভারেজ ট্র্যাকার বাস্তবে তৈরি করা- ব্রাঞ্চ কভারেজ কী এবং এটি স্টেটমেন্ট কভারেজ থেকে কীভাবে আলাদা
- একটি concrete উদাহরণে দেখা যে ১০০% স্টেটমেন্ট কভারেজ থাকলেও ব্রাঞ্চ কভারেজ অসম্পূর্ণ থাকতে পারে
১ · স্টেটমেন্ট কভারেজ: sys.settrace() দিয়ে সত্যিকারের পরিমাপ
স্টেটমেন্ট কভারেজStatement Coverageকোডের মোট এক্সিকিউটেবল লাইনের মধ্যে টেস্ট রান চলাকালীন কতগুলো লাইন আসলে এক্সিকিউট হয়েছে তার অনুপাত।
হলো সবচেয়ে বেসিক কভারেজ মেট্রিক — M5 পর্যন্ত আমরা টেস্ট লিখেছি এবং চালিয়েছি, কিন্তু কখনো মাপিনি
ঠিক কতটুকু কোড সেই টেস্টগুলো আসলে "স্পর্শ" করেছে। বাস্তব জগতে টিমরা এই কাজের জন্য coverage.py-এর
মতো ডেডিকেটেড টুল ব্যবহার করে, কিন্তু এই sandbox-এ থার্ড-পার্টি প্যাকেজ ইনস্টল করা যায় না — তাই নিচে আমরা
Python-এর বিল্ট-ইন sys.settrace() ফাংশন দিয়ে ঠিক সেই একই অন্তর্নিহিত মেকানিজম
(কোড এক্সিকিউট হওয়ার সময় প্রতিটি লাইনের উপর একটি "hook" বসিয়ে দেওয়া) নিজে হাতে বাস্তবায়ন করব।
sys.settrace(tracer) কল করলে Python একটি গ্লোবাল ট্রেস ফাংশন রেজিস্টার করে,
যা নতুন প্রতিটি ফাংশন-কলের শুরুতে ('call' ইভেন্ট) invoked হয়। এই ইভেন্ট থেকে যদি আমরা আবার
tracer-কেই রিটার্ন করি, Python সেই একই ফ্রেমের ভেতরে প্রতিটি লাইন এক্সিকিউট হওয়ার সময়ও
('line' ইভেন্ট) tracer-কে কল করতে থাকে — এভাবেই আমরা প্রতিটি লাইন নম্বর রেকর্ড করতে
পারি। কাজ শেষ হলে sys.settrace(None) কল করে ট্রেসিং বন্ধ করে দেওয়া অত্যাবশ্যক —
নাহলে এটি পুরো প্রোগ্রামের বাকি সব ফাংশন-কলেও চলতে থাকবে এবং অপ্রয়োজনীয় ওভারহেড/সাইড-এফেক্ট তৈরি করবে।
import sys
def measure_coverage(func, test_calls):
# test_calls: একটি লিস্ট, প্রতিটি এলিমেন্ট (args_tuple, kwargs_dict)
target_code = func.__code__
executed_lines = set()
edges = set()
def tracer(frame, event, arg):
if event == 'call':
if frame.f_code is target_code:
tracer.prev_line = None
return tracer # ফ্রেমটি আমাদের ফাংশনের -- এর 'line' ইভেন্টও চাই
return None # অন্য কোনো ফাংশন-কল -- এর ভেতরে ট্রেস করার দরকার নেই
if event == 'line' and frame.f_code is target_code:
lineno = frame.f_lineno
executed_lines.add(lineno)
if tracer.prev_line is not None:
edges.add((tracer.prev_line, lineno))
tracer.prev_line = lineno
return tracer
tracer.prev_line = None
results = []
for args, kwargs in test_calls:
sys.settrace(tracer)
try:
results.append(func(*args, **kwargs))
finally:
sys.settrace(None) # প্রতিটি মাপার পর ট্রেসিং বন্ধ করা আবশ্যক
return executed_lines, edges, results
def count_executable_lines(func):
# func.__code__.co_lines() কম্পাইল-করা বাইটকোড থেকে সরাসরি প্রতিটি প্রকৃত এক্সিকিউটেবল
# লাইন-নাম্বার দেয় (Python 3.10+, PEP 626) -- এর জন্য সোর্স ফাইল পড়ার দরকার নেই, তাই এই
# লেসন-স্যান্ডবক্সে exec() দিয়ে চালানো কোডের ক্ষেত্রেও (যেখানে inspect.getsource() কাজ করে
# না, কারণ কোনো real ফাইল নেই) এটি নির্ভুলভাবে কাজ করে। এই একই লাইন-নাম্বারগুলো
# sys.settrace()-এর 'line' ইভেন্টেও (frame.f_lineno) দেখা যাবে, তাই দুটো সংখ্যা তুলনাযোগ্য।
code = func.__code__
line_numbers = set()
for _start, _end, lineno in code.co_lines():
if lineno is not None and lineno != code.co_firstlineno:
line_numbers.add(lineno)
return sorted(line_numbers)
def grade_score(score):
if score >= 90:
grade = "A"
elif score >= 75:
grade = "B"
else:
grade = "C"
return grade
executable_lines = count_executable_lines(grade_score)
print("grade_score-এর executable লাইন সংখ্যা:", len(executable_lines), "-> লাইন:", executable_lines)
test_calls = [((95,), {}), ((80,), {}), ((50,), {})]
executed_lines, edges, results = measure_coverage(grade_score, test_calls)
print("প্রতিটি কলের রেজাল্ট:", results)
print("মোট এক্সিকিউটেড লাইন (ইউনিয়ন):", sorted(executed_lines))
coverage_pct = len(executed_lines) / len(executable_lines) * 100
print(f"স্টেটমেন্ট কভারেজ: {len(executed_lines)}/{len(executable_lines)} = {coverage_pct:.1f}%")
৯৫, ৮০, ৫০) মিলিয়ে grade_score-এর
if, elif, দুই ব্র্যাঞ্চের অ্যাসাইনমেন্ট, এবং return — মোট ৬টি
এক্সিকিউটেবল লাইনের সবগুলোই অন্তত একবার চলে গেছে, তাই ইউনিয়ন নেওয়ার পর স্টেটমেন্ট কভারেজ ১০০%।
লক্ষ্য করুন — এটি হার্ডকোড করা কোনো সংখ্যা নয়; sys.settrace() প্রতিটি লাইন সত্যিই এক্সিকিউট হওয়ার
সময় রেকর্ড করে এই সংখ্যাটি বের করেছে।
২ · ব্রাঞ্চ কভারেজ: শুধু লাইন চলা যথেষ্ট নয়
ব্রাঞ্চ কভারেজBranch Coverageএকটি কন্ট্রোল-ফ্লো সিদ্ধান্তের (if/else) প্রতিটি সম্ভাব্য দিক — True ও False উভয়ই — টেস্ট রানে অন্তত একবার নেওয়া হয়েছে কি না তার অনুপাত।
স্টেটমেন্ট কভারেজের চেয়ে এক ধাপ কঠোর। একটি if স্টেটমেন্টের দুটো সম্ভাব্য "পথ" আছে — শর্তটি
True হলে যা ঘটে, আর False হলে যা ঘটে (এমনকি যদি সেই if-এর কোনো else ব্লকই না থাকে,
"কিছু না করে এগিয়ে যাওয়া"-ও একটি বৈধ ব্রাঞ্চ)। নিচের apply_discount ফাংশনে ঠিক এই ফাঁদটি
দেখানো হয়েছে — এখানে কোনো else নেই, শুধু একটি if।
"কোডের এই লাইনটি কি অন্তত একবার চলেছে?"
"এই সিদ্ধান্তের উভয় দিক (True ও False) কি অন্তত একবার করে নেওয়া হয়েছে?"
# (এই সেলটি আগের সেলে ডিফাইন করা measure_coverage() ও count_executable_lines() পুনরায় ব্যবহার করছে)
def apply_discount(price, is_vip):
if is_vip:
price = price * 0.9
return price
exec_lines_discount = count_executable_lines(apply_discount)
print("apply_discount-এর executable লাইন:", exec_lines_discount)
if_line, body_line, after_line = exec_lines_discount
# --- টেস্ট সেট ১: শুধু is_vip=True দিয়ে একবার কল করা হলো ---
executed1, edges1, results1 = measure_coverage(apply_discount, [((100, True), {})])
stmt_cov1 = len(executed1) / len(exec_lines_discount) * 100
true_branch_1 = (if_line, body_line) in edges1 # if -> discount লাইন (True পথ)
false_branch_1 = (if_line, after_line) in edges1 # if -> সরাসরি return (False পথ)
branch_cov1 = (true_branch_1 + false_branch_1) / 2 * 100
print("\n[টেস্ট সেট ১: শুধু is_vip=True]")
print("এক্সিকিউটেড লাইন:", sorted(executed1), f"-> স্টেটমেন্ট কভারেজ: {stmt_cov1:.0f}%")
print(f"True ব্রাঞ্চ কভার হয়েছে: {true_branch_1} | False ব্রাঞ্চ কভার হয়েছে: {false_branch_1}")
print(f"ব্রাঞ্চ কভারেজ: {branch_cov1:.0f}% (স্টেটমেন্ট কভারেজ {stmt_cov1:.0f}% হওয়া সত্ত্বেও!)")
# --- টেস্ট সেট ২: is_vip=True এবং is_vip=False দুটোই কল করা হলো ---
executed2, edges2, results2 = measure_coverage(
apply_discount, [((100, True), {}), ((100, False), {})]
)
stmt_cov2 = len(executed2) / len(exec_lines_discount) * 100
true_branch_2 = (if_line, body_line) in edges2
false_branch_2 = (if_line, after_line) in edges2
branch_cov2 = (true_branch_2 + false_branch_2) / 2 * 100
print("\n[টেস্ট সেট ২: is_vip=True এবং is_vip=False দুটোই]")
print("এক্সিকিউটেড লাইন:", sorted(executed2), f"-> স্টেটমেন্ট কভারেজ: {stmt_cov2:.0f}%")
print(f"ব্রাঞ্চ কভারেজ: {branch_cov2:.0f}%")
apply_discount(100, True) কল করলে ফাংশনের তিনটি এক্সিকিউটেবল লাইনই
(if, ডিসকাউন্ট অ্যাসাইনমেন্ট, return) চলে যায় — তাই স্টেটমেন্ট কভারেজ ১০০%।
কিন্তু is_vip=False কখনো টেস্ট করা হয়নি বলে "if -> সরাসরি return" পথ (False ব্রাঞ্চ) একবারও
নেওয়া হয়নি — ব্রাঞ্চ কভারেজ মাত্র ৫০%। টেস্ট সেট ২-এ দ্বিতীয় কলটি (is_vip=False) যোগ করার পর
দুটো ব্রাঞ্চই কভার হয়ে যায়, এবং ব্রাঞ্চ কভারেজও ১০০%-এ পৌঁছায়। এটিই দেখায় —
১০০% স্টেটমেন্ট কভারেজ ১০০% ব্রাঞ্চ কভারেজ গ্যারান্টি করে না।
স্টেটমেন্ট কভারেজ মাপে "কোড চলেছে কি না", ব্রাঞ্চ কভারেজ মাপে "সিদ্ধান্তের প্রতিটি দিক পরীক্ষিত হয়েছে কি না" —
দুটো ভিন্ন প্রশ্ন। একটি else-বিহীন if-এ শুধু True পথ টেস্ট করলেই সব লাইন চলে যায়
বলে স্টেটমেন্ট কভারেজ সহজেই ১০০% দেখাতে পারে, অথচ "কিছু না করা" পথটি (False দিক) কখনো যাচাই না হয়েই থেকে
যেতে পারে — এই কারণেই বাস্তব টিমগুলো শুধু স্টেটমেন্ট কভারেজের উপর নির্ভর না করে ব্রাঞ্চ কভারেজও দেখে। M6-এর
পরের পাঠগুলোতে (পাথ, কন্ডিশন/ডিসিশন কভারেজ) আরও গভীরে এই ধারণা প্রসারিত হবে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি টিম রিপোর্ট করলো তাদের মডিউলের স্টেটমেন্ট কভারেজ ১০০% — এর মানে কি সেই মডিউল সম্পূর্ণভাবে টেস্ট করা হয়ে গেছে?
না, একেবারেই নয়। ১০০% স্টেটমেন্ট কভারেজ শুধু বলে প্রতিটি লাইন অন্তত একবার এক্সিকিউট হয়েছে — এটি বলে না যে প্রতিটি সিদ্ধান্তের উভয় দিক (ব্রাঞ্চ কভারেজ) পরীক্ষা করা হয়েছে, এবং এটি এমনকি বলে না যে ফলাফল সঠিক কি না যাচাই করা হয়েছে (এই শেষ বিষয়টি নিয়ে বিস্তারিত থাকবে L26-এ)। কভারেজ সংখ্যা একটি দরকারী সংকেত, কিন্তু কখনোই একক "টেস্টিং সম্পূর্ণ হয়ে গেছে"-এর প্রমাণ নয়।
প্র ০২
উপরের tracer ফাংশনটি 'call' ইভেন্টে কেন নিজেকে (tracer)
রিটার্ন করে?
এটি Python-এর sys.settrace() API-এর একটি নিয়ম — গ্লোবাল ট্রেস ফাংশন প্রতিটি নতুন ফ্রেমের
'call' ইভেন্টে একবার invoked হয়, এবং সেই ফ্রেমের ভেতরের 'line'/'return'
ইভেন্ট পেতে চাইলে 'call'-এর রিটার্ন ভ্যালু হিসেবে একটি (লোকাল) ট্রেস ফাংশন দিতে হয় — যদি
None রিটার্ন করা হয়, সেই নির্দিষ্ট ফ্রেমের ভেতরের কোনো লাইন-ইভেন্টই আর আসবে না। উপরের কোডে
আমরা আমাদের টার্গেট ফাংশনের ফ্রেমের জন্য tracer রিটার্ন করি (লাইন-ট্র্যাকিং চালু রাখতে), আর
অন্য যেকোনো ফাংশন-কলের জন্য None রিটার্ন করি (অপ্রয়োজনীয় ট্রেসিং এড়াতে)।
প্র ০৩
apply_discount-এর টেস্ট সেট ১-এ যদি প্রোডাকশনে সত্যিই is_vip=False কোনো
ব্যবহারকারী আসে, তাহলে কী ঝুঁকি থেকে যায়?
is_vip=False পথটি (ছাড় ছাড়া মূল দাম ফেরত দেওয়া) কখনো টেস্ট রানে এক্সিকিউট হয়নি বলে সেই
পথে কোনো লুকানো বাগ থাকলেও তা কভারেজ রিপোর্টে ধরা পড়বে না — টিম মিথ্যা আত্মবিশ্বাস নিয়ে ভাবতে পারে
ফাংশনটি "সম্পূর্ণ টেস্ট করা"। যদি ভবিষ্যতে কেউ return price লাইনটি ভুলভাবে পরিবর্তন করে
(যেমন ভুলবশত return price * 0.9 সব ক্ষেত্রেই করে দেয়), এই না-টেস্ট-করা পথে সেই রিগ্রেশনটি
অলক্ষিতই থেকে যাবে।
অনুশীলন
-
চিন্তা করুন: একটি ফাংশনে
if a or b:লাইন আছে। যদি টেস্ট স্যুটে শুধু একটি কল থাকে যেখানেa=True, b=False, তাহলে এই লাইনটির স্টেটমেন্ট কভারেজ কি ১০০%? এই সিদ্ধান্তের ব্রাঞ্চ কভারেজ সম্পর্কে কী বলা যায়?হ্যাঁ,
if a or b:লাইনটি এক্সিকিউট হয়েছে (তার মান যা-ই হোক), তাই সেই লাইনের স্টেটমেন্ট কভারেজ ১০০%। কিন্তু ব্রাঞ্চ কভারেজের প্রশ্নে — এই একটি কলে সিদ্ধান্তটি True হয়েছে (যেহেতুa=True), False কখনো হয়নি — তাই ব্রাঞ্চ কভারেজ মাত্র ৫০% (দুটো ব্রাঞ্চের একটি কভার হয়েছে)। L25-এ এই একই কম্পাউন্ড কন্ডিশনের ভেতরের প্রতিটি অংশ (a, b) আলাদাভাবে পরীক্ষা করার "কন্ডিশন কভারেজ" ধারণা শেখানো হবে। -
পরীক্ষা করুন: দ্বিতীয় কোড সেলে
test_calls-এ আরেকটি কল যোগ করুন —((250, True), {})— এবং Run চেপে দেখুন স্টেটমেন্ট বা ব্রাঞ্চ কভারেজের সংখ্যা বদলায় কি না।না, সংখ্যা বদলাবে না —
(250, True)কলটিও একই দুটো লাইন (ifএবং ডিসকাউন্ট অ্যাসাইনমেন্ট) এক্সিকিউট করে যেগুলো ইতিমধ্যে(100, True)কলে কভার হয়ে গিয়েছিল — নতুন কোনো লাইন বা নতুন কোনো ব্রাঞ্চ (True/False এজ) যোগ হয়নি। এটি একটি গুরুত্বপূর্ণ শিক্ষা: শুধু আরও বেশি টেস্ট কেস যোগ করলেই কভারেজ বাড়ে না — নতুন টেস্ট কেসকে অবশ্যই না-চলা কোনো লাইন বা ব্রাঞ্চ স্পর্শ করতে হবে কভারেজ বাড়ানোর জন্য।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, ইন্টিগ্রেশন টেস্টিং, কভারেজ, অটোমেশন, পারফরম্যান্স ও সিকিউরিটি টেস্টিং, অ্যাডভান্সড টেকনিক ও CI/CD ইন্টিগ্রেশন — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
- বাউন্ডারি ভ্যালু অ্যানালাইসিস L06 থ্রেশহোল্ডের আশেপাশের মান বেছে নেওয়ার টেকনিক — সেখানে বেছে নেওয়া টেস্ট কেসগুলো আজকের কভারেজ ট্র্যাকার দিয়ে মাপলে কতটা কভারেজ পায় তা নিজে যাচাই করে দেখতে পারেন।
- সব 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 — সব এক জায়গায়।