মিউটেশন টেস্টিং
এই পাঠে যা শিখবেন
- মিউটেশন টেস্টিং কী এবং এটি কেন কভারেজের চেয়ে গভীর একটি মেট্রিক
- "mutant", "killed" ও "survived" শব্দগুলোর সঠিক অর্থ
- একটি সত্যিকারের, চলমান মিউটেশন টেস্টিং পাস — একই টেস্ট স্যুট একাধিক মিউট্যান্টের বিরুদ্ধে চালানো
- একটি survived mutant থেকে টেস্ট স্যুটের দুর্বলতা কীভাবে চিহ্নিত ও সমাধান করতে হয়
১ · মিউটেশন টেস্টিং কী
মিউটেশন টেস্টিংMutation Testingকোডে ইচ্ছাকৃতভাবে ছোট ছোট বাগ (mutant) ঢুকিয়ে, বিদ্যমান টেস্ট স্যুট সেগুলো ধরতে পারে কি না যাচাই করে টেস্ট স্যুটের নিজের মান পরিমাপ করা।
প্রশ্নটা "কোড কি সঠিক?" নয় — প্রশ্নটা "আমার টেস্ট স্যুট কি যথেষ্ট ভালো?" প্রতিটি
mutantMutantমূল কোডের একটি কপি, যেখানে ঠিক একটি ছোট, ইচ্ছাকৃত পরিবর্তন (যেমন একটি তুলনা অপারেটর উল্টে দেওয়া) করা হয়েছে।
মূল ফাংশনের একটি কপি যেখানে ঠিক একটি ছোট পরিবর্তন করা হয়েছে — একটি তুলনা অপারেটর উল্টানো
(< থেকে <=), একটি বুলিয়ান অপারেটর পাল্টানো (and থেকে
or), অথবা একটি অফ-বাই-ওয়ান ভুল ঢোকানো। বিদ্যমান টেস্ট স্যুট প্রতিটি মিউট্যান্টের বিরুদ্ধে
আলাদাভাবে চালানো হয়:
অন্তত একটি টেস্ট কেস ব্যর্থ হয়েছে মিউট্যান্টের বিরুদ্ধে — মানে টেস্ট স্যুট এই নির্দিষ্ট বাগটি ধরতে সক্ষম। ভালো লক্ষণ।
সব টেস্ট কেস পাস করেছে, যদিও কোডে একটি প্রকৃত বাগ (মিউট্যান্ট) ছিল — টেস্ট স্যুটে একটি ফাঁক আছে যা কভারেজ রিপোর্টে হয়তো দেখাই যেত না।
killed mutant-এর সংখ্যা ÷ মোট mutant-এর সংখ্যা — যত বেশি, টেস্ট স্যুট তত বেশি নির্ভরযোগ্যভাবে বাগ ধরতে সক্ষম বলে ধরে নেওয়া হয়।
M6/L26-এ দেখানো
হয়েছিল ১০০% স্টেটমেন্ট কভারেজ থাকলেও একটি দুর্বল assert বাগ মিস করতে পারে। মিউটেশন টেস্টিং
সরাসরি সেই দুর্বলতা পরিমাপ করে — একটি লাইন "চলেছে" কি না তা নয়, বরং সেই লাইনে বাগ ঢুকলে টেস্ট স্যুট তা
ধরতে পারে কি না। এই কারণে মিউটেশন স্কোর কভারেজ শতাংশের চেয়ে টেস্ট স্যুটের মান সম্পর্কে অনেক বেশি
তথ্য দেয় — যদিও তা চালাতে বেশি কম্পিউটেশন সময় লাগে (প্রতিটি মিউট্যান্টের জন্য পুরো স্যুট আলাদাভাবে চলে)।
২ · বাস্তবায়ন: একটি পাসওয়ার্ড ভ্যালিডেটরে তিনটি মিউট্যান্ট
নিচে একটি ছোট is_valid_password ফাংশন — নিয়ম: পাসওয়ার্ড কমপক্ষে ৮ অক্ষর লম্বা হতে হবে, এবং তাতে
অন্তত একটি সংখ্যা ও একটি বড় হাতের অক্ষর থাকতে হবে। এর জন্য একটি চার-টেস্টের স্যুট (প্লেইন
assert-স্টাইল) লেখা আছে, এরপর তিনটি ভিন্ন মিউট্যান্ট সংজ্ঞায়িত করা হয়েছে — প্রতিটিতে ঠিক একটি
পরিবর্তন — এবং একই টেস্ট স্যুট প্রতিটি মিউট্যান্টের বিরুদ্ধে সত্যিকারভাবে চালানো হয়েছে।
def is_valid_password(password):
if len(password) < 8:
return False
has_digit = any(ch.isdigit() for ch in password)
has_upper = any(ch.isupper() for ch in password)
return has_digit and has_upper
TEST_CASES = [
("test_valid_password", "Abcdefg1", True),
("test_too_short", "Ab1", False),
("test_no_digit", "Abcdefgh", False),
("test_no_upper", "abcdefg1", False),
]
def run_test_suite(implementation):
results = []
for name, password, expected in TEST_CASES:
actual = implementation(password)
results.append((name, actual == expected, expected, actual))
return results
print("বিদ্যমান ইমপ্লিমেন্টেশনের বিরুদ্ধে বেসলাইন রান:")
for name, passed, expected, actual in run_test_suite(is_valid_password):
print(f" {name}: {'PASS' if passed else 'FAIL'} (expected={expected}, actual={actual})")
বেসলাইন সবসময় চারটি টেস্টেই PASS দেখানো উচিত — এটিই স্বাভাবিক, কারণ ফাংশনটি সঠিক এবং টেস্ট স্যুটটি তার
বিরুদ্ধেই লেখা। এবার আসল পরীক্ষা — নিচে তিনটি মিউট্যান্ট এবং একই run_test_suite
ফাংশন প্রতিটির বিরুদ্ধে চালানো হচ্ছে।
def mutant_off_by_one(password):
# পরিবর্তন: < 8 -> < 7 (অফ-বাই-ওয়ান, থ্রেশহোল্ড এক ধাপ দুর্বল হয়ে গেছে)
if len(password) < 7:
return False
has_digit = any(ch.isdigit() for ch in password)
has_upper = any(ch.isupper() for ch in password)
return has_digit and has_upper
def mutant_comparison_flip(password):
# পরিবর্তন: < -> <= (তুলনা অপারেটর উল্টানো)
if len(password) <= 8:
return False
has_digit = any(ch.isdigit() for ch in password)
has_upper = any(ch.isupper() for ch in password)
return has_digit and has_upper
def mutant_boolean_flip(password):
# পরিবর্তন: and -> or (বুলিয়ান অপারেটর পাল্টানো)
if len(password) < 8:
return False
has_digit = any(ch.isdigit() for ch in password)
has_upper = any(ch.isupper() for ch in password)
return has_digit or has_upper
MUTANTS = {
"mutant_off_by_one (len < 7 এর বদলে < 8)": mutant_off_by_one,
"mutant_comparison_flip (<= এর বদলে <)": mutant_comparison_flip,
"mutant_boolean_flip (or এর বদলে and)": mutant_boolean_flip,
}
killed = 0
for mutant_name, mutant_func in MUTANTS.items():
results = run_test_suite(mutant_func)
mutant_killed = any(not passed for _, passed, _, _ in results)
if mutant_killed:
killed += 1
print(f"{mutant_name}: {'KILLED' if mutant_killed else 'SURVIVED'}")
for name, passed, expected, actual in results:
if not passed:
print(f" caught by {name} (expected={expected}, actual={actual})")
score = killed / len(MUTANTS)
print(f"\nMutation score: {killed}/{len(MUTANTS)} = {score:.1%}")
mutant_comparison_flip এবং mutant_boolean_flip — দুটোই
KILLED হয় (যথাক্রমে test_valid_password এবং test_no_digit/
test_no_upper এগুলো ধরে ফেলে)। কিন্তু mutant_off_by_one
SURVIVED করে — মোট মিউটেশন স্কোর দাঁড়ায় ২/৩ = ৬৬.৭%।
৩ · একটি survived mutant কী প্রকাশ করে
mutant_off_by_one survive করার কারণ পরিষ্কার — বিদ্যমান চারটি টেস্ট কেসের মধ্যে একটিও ঠিক
৭ অক্ষর দৈর্ঘ্যের একটি পাসওয়ার্ড (যাতে সংখ্যা ও বড় হাতের অক্ষরও আছে) ব্যবহার করে না।
আসল ফাংশনে ৭-অক্ষরের পাসওয়ার্ড অবশ্যই প্রত্যাখ্যাত হওয়া উচিত (len < 8), কিন্তু মিউট্যান্টে
থ্রেশহোল্ড ৭-এ নেমে যাওয়ায় সেটি ভুলভাবে গ্রহণ করে ফেলে — অথচ কোনো টেস্ট কেসই এই নির্দিষ্ট বাউন্ডারিতে
ফাংশনটিকে পরীক্ষা করে না, তাই বাগটি অলক্ষিত থেকে যায়। এটি সরাসরি M2/L06-এর
বাউন্ডারি ভ্যালু অ্যানালাইসিস-এর অভাব — থ্রেশহোল্ডের ঠিক নিচের মান কখনো টেস্ট করা হয়নি।
# নতুন বাউন্ডারি টেস্ট যোগ করলে এই মিউট্যান্ট কি ধরা পড়ে?
new_test_password = "Abcdef1" # ঠিক 7 অক্ষর, একটি সংখ্যা ও একটি বড় হাতের অক্ষরসহ
expected = False # 7 < 8, তাই বাতিল হওয়ার কথা
real_result = is_valid_password(new_test_password)
mutant_result = mutant_off_by_one(new_test_password)
print(f"আসল ফাংশন: {real_result} (expected {expected}) -> {'ঠিক আছে' if real_result == expected else 'ভুল!'}")
print(f"mutant_off_by_one: {mutant_result} (expected {expected}) -> {'ঠিক আছে' if mutant_result == expected else 'ধরা পড়ল — mutant killed!'}")
False রিটার্ন করে (৭ অক্ষর যথেষ্ট নয়) — কিন্তু mutant_off_by_one
ভুলভাবে True রিটার্ন করে, তাই এই নতুন টেস্ট কেসটি যোগ করলে test_boundary_length_7
নামে টেস্ট স্যুটে রাখলে মিউটেশন স্কোর ৩/৩ = ১০০%-এ উঠে যাবে। এটিই মিউটেশন টেস্টিং-এর ব্যবহারিক মূল্য — শুধু
"স্কোর কম" বলে থেমে না গিয়ে, ঠিক কোন নির্দিষ্ট টেস্ট কেসটি অনুপস্থিত তা নির্দেশ করে দেয়।
মিউটেশন টেস্টিং একটি টেস্ট স্যুটকে "সংখ্যা" (কত লাইন কভার হলো) দিয়ে নয়, "ক্ষমতা" (আসলে বাগ ধরতে পারে কি
না) দিয়ে বিচার করে। বাস্তব-জগতে PIT (Java), mutmut বা cosmic-ray (Python)-এর
মতো ডেডিকেটেড টুল স্বয়ংক্রিয়ভাবে শত শত মিউট্যান্ট তৈরি করে — এই পাঠের কোড সেই একই মৌলিক প্রক্রিয়াটির একটি
হাতে-লেখা, ছোট-স্কেলের কিন্তু সম্পূর্ণ কার্যকরী সংস্করণ।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১ একটি টিম বলছে "আমাদের কোড কভারেজ ১০০%, তাই আমাদের টেস্ট স্যুট নিখুঁত" — মিউটেশন টেস্টিং এই দাবিকে কীভাবে চ্যালেঞ্জ করতে পারে?
১০০% কভারেজ শুধু বলে প্রতিটি লাইন অন্তত একবার চলেছে — এটি বলে না যে সেই লাইনের ফলাফল সঠিকভাবে যাচাই
হয়েছে। এই পাঠের উদাহরণেই দেখা গেছে — is_valid_password-এর সবগুলো লাইন চারটি টেস্টেই চলে
(১০০% স্টেটমেন্ট কভারেজ থাকতে পারে), তবু mutant_off_by_one survive করেছে কারণ থ্রেশহোল্ড
বাউন্ডারিতে কোনো টেস্ট নেই। মিউটেশন টেস্টিং এই ধরনের "লাইন চলেছে কিন্তু ঠিকমতো যাচাই হয়নি" ফাঁকগুলো
সরাসরি প্রকাশ করে দেয়।
প্র ০২ মিউটেশন টেস্টিং প্রতিটি মিউট্যান্টের বিরুদ্ধে পুরো টেস্ট স্যুট আলাদাভাবে চালায় — এর ব্যবহারিক খরচ কী হতে পারে, এবং বড় প্রজেক্টে এটি কীভাবে সামলানো হয়?
শত শত মিউট্যান্ট থাকলে এবং প্রতিটির বিরুদ্ধে পুরো স্যুট (যা নিজেও হাজার হাজার টেস্ট হতে পারে) চালাতে হলে মোট রানটাইম অনেক বেড়ে যায় — একটি ছোট বাগ ফিক্সের জন্যও পুরো মিউটেশন পাস চালানো ব্যবহারিক নাও হতে পারে। বাস্তব-জগতের মিউটেশন টেস্টিং টুল তাই সাধারণত অপ্টিমাইজেশন ব্যবহার করে — শুধু পরিবর্তিত ফাইলের সংশ্লিষ্ট মিউট্যান্ট চালানো, দ্রুত-ব্যর্থ টেস্ট আগে চালানো, বা নিয়মিত পুরো রান না করে মাঝে মাঝে (যেমন সাপ্তাহিক) চালানো।
প্র ০৩ যদি একটি মিউট্যান্ট এমনভাবে তৈরি হয় যে তা মূল ফাংশনের সাথে আচরণগতভাবে সম্পূর্ণ অভিন্ন থাকে (কোনো ইনপুটেই ফলাফল পাল্টায় না), তাহলে সেটি কি "survived" গণনা করা উচিত?
এই ধরনের মিউট্যান্টকে বলা হয় "equivalent mutant" — যেহেতু আচরণ কখনোই পাল্টায় না, কোনো টেস্ট স্যুট (এমনকি একটি নিখুঁত স্যুটও) কখনোই সেটি "kill" করতে পারবে না। এটি মিউটেশন টেস্টিং-এর একটি স্বীকৃত ব্যবহারিক সীমাবদ্ধতা — এই ধরনের মিউট্যান্ট মিউটেশন স্কোরকে কৃত্রিমভাবে কমিয়ে দেয় কারণ এটি আসলে টেস্ট স্যুটের কোনো প্রকৃত দুর্বলতা প্রকাশ করে না। বাস্তব টুলগুলো এই সমস্যা কমাতে বিভিন্ন হিউরিস্টিক ব্যবহার করে, কিন্তু সম্পূর্ণভাবে দূর করা কঠিন।
অনুশীলন
-
চিন্তা করুন: উপরের
MUTANTSডিকশনারিতে চতুর্থ একটি মিউট্যান্টmutant_negate_upperযোগ করতে চাইলে (শুধুhas_digitচেক করে,has_upperসম্পূর্ণ উপেক্ষা করে —return has_digit), এটি কোন বিদ্যমান টেস্ট কেস দিয়ে ধরা পড়বে বলে মনে হয়?test_no_upper("abcdefg1", expectedFalse) — এই মিউট্যান্টেhas_digitএখানেTrue(একটি সংখ্যা আছে), তাইreturn has_digitসরাসরিTrueরিটার্ন করবে, কিন্তু এক্সপেক্টেডFalse— টেস্ট ব্যর্থ হবে, তাই এই মিউট্যান্ট KILLED হবে। -
পরীক্ষা করুন: দ্বিতীয় কোড সেলে
mutant_off_by_one-এর সংজ্ঞায়< 7-কে< 8-এ ফিরিয়ে দিন (অর্থাৎ মূল ফাংশনের সাথে হুবহু মিলিয়ে দিন) এবং Run চেপে দেখুন — এখন এটিকে কি "killed" নাকি "survived" রিপোর্ট করা হয়?এটি এখনও "SURVIVED" হিসেবেই রিপোর্ট হবে — কিন্তু এখন ঠিক ভিন্ন কারণে: যেহেতু কোডটি মূল ফাংশনের সাথে হুবহু অভিন্ন হয়ে গেছে, এটি আসলে আর "মিউট্যান্ট" নয় বরং একটি "equivalent mutant" (উপরের প্রশ্ন ০৩ দেখুন) — কোনো টেস্টই কখনো এটি ধরতে পারবে না, কারণ ধরার মতো কোনো আচরণগত পার্থক্যই নেই।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরের পাঠ পড়ুন ফাজ টেস্টিং এলোমেলো, অস্বাভাবিক ইনপুট দিয়ে ফাংশনকে "আক্রমণ" করে অপ্রত্যাশিত ক্র্যাশ খুঁজে বের করার কৌশল।
- M6/L26 আবার দেখুন কভারেজের সীমাবদ্ধতা ১০০% কভারেজ কেন যথেষ্ট নয় তার আরেকটি কনক্রিট উদাহরণ — এই পাঠের সাথে সরাসরি সম্পর্কিত।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ টেস্ট ডিজাইন টেকনিক, ইউনিট টেস্টিং, টেস্ট ডাবলস, কভারেজ, অটোমেশন, পারফরম্যান্স, সিকিউরিটি ও অ্যাডভান্সড টেকনিক — সব একসাথে।