পাঠ ৪৩ · ৫৭-এর মধ্যে · মডিউল ১০
Home / Courses / Software Testing & Quality Assurance / ফাজ টেস্টিং

ফাজ টেস্টিং

Fuzz testing
১০ মিনিট পড়া অ্যাডভান্সড Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ফাজ টেস্টিং কী এবং প্রপার্টি-বেসড টেস্টিং থেকে এটি কীভাবে আলাদা
  • "ডকুমেন্টেড/প্রত্যাশিত এরর" বনাম "অপ্রত্যাশিত ক্র্যাশ"-এর পার্থক্য কেন গুরুত্বপূর্ণ
  • stdlib-only একটি ফিক্সড-seed ফাজার বাস্তবায়ন করা
  • একটি প্রকৃত ইনপুট-হ্যান্ডলিং বাগ কীভাবে ফাজিং দিয়ে ধরা পড়ে তার লাইভ ডেমো

১ · ফাজ টেস্টিং কী

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

গ্রেসফুল হ্যান্ডলিং
ফাংশন হয় সঠিক ফলাফল দেয়, নয়তো একটি ডকুমেন্টেড/প্রত্যাশিত এক্সেপশন (যেমন ValueError) তোলে যা কলার আশা করতে পারে ও ধরতে পারে।
অপ্রত্যাশিত ক্র্যাশ
ফাংশন এমন একটি এক্সেপশন তোলে যা ডকুমেন্টেশনে নেই বা প্রত্যাশিত নয় (যেমন TypeError, AttributeError) — কলার এটি ধরার কথা ভাবেওনি, তাই এটি প্রোডাকশনে সরাসরি ক্র্যাশ বা সিকিউরিটি সমস্যা হতে পারে।

বাস্তব-জগতে ফাজ টেস্টিং সবচেয়ে বেশি পরিচিত সিকিউরিটি প্রেক্ষাপটে (M9-এ সংক্ষেপে উল্লেখ করা হয়েছিল) — AFL, libFuzzer-এর মতো টুল কম্পাইল করা প্রোগ্রামের বাইনারি ইনপুট নিয়ে কাজ করে এবং কোন ইনপুট কোড-পাথের নতুন অংশে পৌঁছাচ্ছে তা ট্র্যাক করে স্মার্টভাবে ইনপুট বিবর্তিত করে ("coverage-guided fuzzing")। আমাদের এই পাঠের সংস্করণ অনেক সরল — শুধু random মডিউল দিয়ে কয়েক ধরনের বিকৃত ইনপুট জেনারেট করা — কিন্তু মূল ধারণাটি (বহু অস্বাভাবিক ইনপুট, অপ্রত্যাশিত ক্র্যাশ গণনা) অভিন্ন।

২ · বাস্তবায়ন: একটি ফিক্সড-seed ফাজার

নিচের parse_age ফাংশনটির ডকুমেন্টেড আচরণ হলো — অবৈধ (নন-নিউমেরিক বা রেঞ্জের বাইরের) ইনপুটে ValueError তোলা। কিন্তু এটি লেখা হয়েছে ধরে নিয়ে যে ইনপুট সবসময় একটি স্ট্রিং বা সংখ্যা হবে — None-এর মতো একটি সম্পূর্ণ ভিন্ন টাইপ দিলে কী হয় তা নিয়ে ভাবা হয়নি। make_fuzz_input ফাংশনটি একটি ফিক্সড seed (random.Random(42)) ব্যবহার করে ছয় ধরনের ইনপুটের মধ্যে থেকে ঘুরিয়ে ঘুরিয়ে বেছে নেয় — এলোমেলো আবর্জনা স্ট্রিং, রেঞ্জের বাইরের বড় সংখ্যা, নেগেটিভ সংখ্যা, খালি স্ট্রিং, None, এবং একটি বৈধ ইন-রেঞ্জ সংখ্যা।

Python
import random

def parse_age(value):
    # ডকুমেন্টেড আচরণ: অবৈধ ইনপুটে ValueError তোলার কথা
    age = int(value)
    if age < 0 or age > 150:
        raise ValueError(f"age {age} is out of realistic range")
    return age

def make_fuzz_input(rng):
    choice = rng.randint(0, 5)
    if choice == 0:
        # অস্বাভাবিক অক্ষরসহ এলোমেলো আবর্জনা স্ট্রিং
        chars = "abcxyz!@#$%^&*() "
        length = rng.randint(0, 8)
        return "".join(rng.choice(chars) for _ in range(length))
    elif choice == 1:
        # রেঞ্জের বাইরের বিশাল সংখ্যা (স্ট্রিং হিসেবে)
        return str(rng.randint(151, 10**9))
    elif choice == 2:
        # নেগেটিভ সংখ্যা (স্ট্রিং হিসেবে)
        return str(-rng.randint(1, 1000))
    elif choice == 3:
        return ""  # খালি ইনপুট
    elif choice == 4:
        return None  # সম্পূর্ণ ভুল টাইপ
    else:
        return str(rng.randint(0, 150))  # একটি স্বাভাবিক, বৈধ ইনপুটও রাখা হলো

rng = random.Random(42)  # ফিক্সড seed -> প্রতিবার একই ৫০টি ইনপুট, reproducible ফলাফল
N = 50
EXPECTED_EXCEPTIONS = (ValueError,)

graceful = 0
crashes = 0
crash_details = []

for i in range(N):
    fuzz_input = make_fuzz_input(rng)
    try:
        parse_age(fuzz_input)
        graceful += 1  # সফলভাবে পার্স হলো -> ঠিক আছে
    except EXPECTED_EXCEPTIONS:
        graceful += 1  # ডকুমেন্টেড এরর তুলেছে -> ঠিক আছে, এটাই প্রত্যাশিত
    except Exception as e:
        crashes += 1   # অপ্রত্যাশিত এক্সেপশন টাইপ!
        crash_details.append((repr(fuzz_input), type(e).__name__, str(e)))

print(f"Fuzzed {N} inputs with a fixed seed (reproducible)")
print(f"Handled gracefully (valid result or documented ValueError): {graceful}/{N}")
print(f"Unexpected crashes (undocumented exception type): {crashes}/{N}")
for inp, exc_name, msg in crash_details[:5]:
    print(f"  unexpected {exc_name} on input {inp} -> {msg}")

    
seed 42-এর সাথে এই কোড প্রতিবারই ৫০-এর মধ্যে ৮টি ইনপুটে ক্র্যাশ রিপোর্ট করে — প্রতিটিই None ইনপুটের কারণে ঘটা একটি TypeError (কারণ Python-এর বিল্ট-ইন int(None) সরাসরি TypeError তোলে, ValueError নয়)। বাকি ৪২টি ইনপুট — আবর্জনা স্ট্রিং, রেঞ্জের বাইরের সংখ্যা, খালি স্ট্রিং — সবগুলোই সঠিকভাবে ডকুমেন্টেড ValueError তোলে অথবা সফলভাবে পার্স হয়, তাই "গ্রেসফুল" হিসেবে গণনা হয়।

৩ · এই ফলাফল আসলে কী প্রকাশ করছে

এই ৮টি "ক্র্যাশ" প্রতিটিই একই মূল কারণ থেকে এসেছে — parse_age শুধু ধরে নিয়েছে ইনপুট সবসময় স্ট্রিং বা সংখ্যা টাইপের হবে, কিন্তু কখনো টাইপ যাচাই করেনি। বাস্তব-জগতে এই ধরনের ফাংশন যদি একটি ওয়েব ফর্মের পেছনে থাকে এবং কোনো ফিল্ড ফাঁকা রেখে None পাঠানো হয় (যেমন একটি অপশনাল ফিল্ড ভুলবশত পাঠানো হলো), তাহলে ব্যবহারকারী একটি অস্পষ্ট TypeError ট্রেসব্যাক দেখবে — একটি পরিষ্কার "বয়স অবশ্যই একটি সংখ্যা হতে হবে" বার্তার বদলে। ফাজিং এই ধরনের "ভাবাই হয়নি এমন ইনপুট" ঠিক এভাবেই খুঁজে বের করে — কোনো নির্দিষ্ট টেস্ট কেস কখনো None চেষ্টা করার কথা না ভাবলেও, ফাজার এলোমেলোভাবে সেটি বেছে নিয়ে বাগটি প্রকাশ করে দেয়।

প্রকৃত ফিক্স সহজ — ফাংশনের শুরুতে একটি স্পষ্ট টাইপ চেক যোগ করে ইচ্ছাকৃতভাবে ValueError তোলা:

Python
def parse_age_fixed(value):
    if not isinstance(value, (str, int)):
        raise ValueError(f"age must be a string or int, got {type(value).__name__}")
    age = int(value)
    if age < 0 or age > 150:
        raise ValueError(f"age {age} is out of realistic range")
    return age

# একই ফাজিং লুপ, এবার fixed সংস্করণে
rng2 = random.Random(42)
graceful2 = 0
crashes2 = 0
for i in range(N):
    fuzz_input = make_fuzz_input(rng2)
    try:
        parse_age_fixed(fuzz_input)
        graceful2 += 1
    except EXPECTED_EXCEPTIONS:
        graceful2 += 1
    except Exception as e:
        crashes2 += 1

print(f"Fixed version -> handled gracefully: {graceful2}/{N}, unexpected crashes: {crashes2}/{N}")

    
parse_age_fixed-এ একই ৫০টি ফাজড ইনপুট (একই seed 42 ব্যবহার করে, তাই ইনপুটের ক্রম হুবহু আগের মতোই) চালালে এবার ০টি অপ্রত্যাশিত ক্র্যাশ রিপোর্ট হয় — সব ৫০টি ইনপুট গ্রেসফুলভাবে সামলানো হয় (হয় সফল পার্স, নয়তো ডকুমেন্টেড ValueError), কারণ None এখন isinstance চেকে ধরা পড়ে ইচ্ছাকৃতভাবে ValueError তোলে, আর কখনোই int(None)-এ পৌঁছায় না।
মূল কথা · Key takeaway

ফাজ টেস্টিং প্রমাণ করে না যে কোড সঠিক — এটি খুঁজে বের করে কোড কোথায় অপ্রত্যাশিতভাবে ভেঙে পড়ে। এলোমেলো ইনপুট জেনারেশনের সাথে একটি ফিক্সড seed মিলিয়ে ব্যবহার করলে ফলাফল reproducible থাকে — একটি বাগ একবার ধরা পড়লে, একই কোড আবার চালিয়ে সেই একই বাগ আবার নিশ্চিতভাবে দেখা যাবে, যা ডিবাগিং ও ফিক্স যাচাই করাকে ব্যবহারিক করে তোলে।

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

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

প্র ০১ EXPECTED_EXCEPTIONS = (ValueError,) লাইনটি ফাজারে ঠিক কোন কাজটি করছে, এবং এটি বাদ দিলে কী সমস্যা হতো?

এটি বলে দেয় কোন এক্সেপশন টাইপ ফাংশনের "ডকুমেন্টেড, প্রত্যাশিত" আচরণের অংশ — শুধু এই টাইপগুলো গ্রেসফুল হিসেবে গণনা হয়, বাকি সব "অপ্রত্যাশিত ক্র্যাশ"। এটি বাদ দিলে (অথবা সব Exception-কে গ্রেসফুল ধরে নিলে) ফাজারটি অর্থহীন হয়ে যেত — তখন TypeError-সহ সব ধরনের ক্র্যাশই "ঠিক আছে" হিসেবে গণনা হতো, এবং প্রকৃত বাগটি (None-এ TypeError) কখনোই ধরা পড়ত না।

প্র ০২ ফাজ টেস্টিং (L43) ও প্রপার্টি-বেসড টেস্টিং (L41) — দুটোই random দিয়ে বহু ইনপুট জেনারেট করে, তাহলে এই দুটো পাঠের মধ্যে মূল পার্থক্য ঠিক কোথায়?

প্রপার্টি-বেসড টেস্টিং একটি ইতিবাচক দাবি যাচাই করে — "আউটপুট সবসময় এই নির্দিষ্ট নিয়ম মেনে চলে" (যেমন সর্ট করা আউটপুট non-decreasing)। ফাজ টেস্টিং তুলনামূলক কম উচ্চাভিলাষী — এটি আউটপুট সঠিক কি না তা মোটেও যাচাই করে না, শুধু দেখে ফাংশনটি অপ্রত্যাশিতভাবে ভেঙে পড়ে কি না। এই কারণে ফাজিং প্রায়ই এমন ফাংশনেও চালানো যায় যেখানে "সঠিক আউটপুট কী" তা লেখা কঠিন, কিন্তু "এটি ক্র্যাশ করা উচিত নয়" বলা সহজ।

প্র ০৩ make_fuzz_input-এর choice == 5 শাখাটি (একটি স্বাভাবিক, বৈধ ইন-রেঞ্জ সংখ্যা) কেন ইচ্ছাকৃতভাবে ফাজারে রাখা হয়েছে?

যদি ফাজার শুধুই খারাপ ইনপুট জেনারেট করত, তাহলে "গ্রেসফুল" গণনার মধ্যে শুধু ব্যতিক্রম-ধরা কেসই থাকত — বৈধ ইনপুটে ফাংশনটি আদৌ কাজ করে কি না তা এই লুপে কখনো যাচাই হতো না। মাঝেমধ্যে একটি বৈধ ইনপুট মিশিয়ে রাখলে নিশ্চিত হওয়া যায় যে ফাংশনটি শুধু "সব রিজেক্ট করে" এমন একটি ভাঙা সংস্করণ নয় — এটি প্রকৃতপক্ষে বৈধ কেসে সঠিক ফলাফলও দেয়।

অনুশীলন

  1. চিন্তা করুন: make_fuzz_input-এ আরেকটি নতুন শাখা যোগ করতে চাইলে — যেমন একটি খুব বড় স্ট্রিং (হাজার হাজার অক্ষর) বা একটি ফাঁকা লিস্ট [] — এগুলো parse_age-এ পাঠালে কী হবে বলে মনে হয়?

    একটি খুব বড় সংখ্যাসূচক স্ট্রিং (যেমন হাজার অঙ্কের একটি সংখ্যা) int()-এ সফলভাবে পার্স হবে (Python-এর int-এর কোনো নির্দিষ্ট সাইজ লিমিট নেই), কিন্তু তারপর age > 150 চেকে ধরা পড়ে সঠিকভাবে ValueError তুলবে — গ্রেসফুল। কিন্তু [] (একটি লিস্ট) পাঠালে int([]) একটি TypeError তুলবে, ঠিক None-এর মতোই — এটি আরেকটি অপ্রত্যাশিত ক্র্যাশ প্রকাশ করবে, যা নিশ্চিত করে যে বাগটি শুধু None-এ সীমাবদ্ধ নয়, বরং "নন-স্ট্রিং/নন-সংখ্যা টাইপ" ইনপুটের সম্পূর্ণ শ্রেণির জন্যই প্রযোজ্য।

  2. পরীক্ষা করুন: প্রথম কোড সেলে rng = random.Random(42)-কে rng = random.Random(7)-এ পরিবর্তন করে Run চেপে দেখুন — ক্র্যাশের সংখ্যা কি বদলায়?

    হ্যাঁ, ভিন্ন seed মানে ভিন্ন ক্রমে ইনপুট জেনারেট হবে, তাই choice == 4 (None) ঠিক কতবার বাছাই হয় তা বদলে যেতে পারে — ক্র্যাশের সংখ্যা ৮ থেকে সামান্য কমবেশি হতে পারে। কিন্তু যতক্ষণ None-শাখাটি অন্তত একবারও বাছাই হয়, ততক্ষণ অন্তত একটি অপ্রত্যাশিত TypeError ধরা পড়বেই — বাগটি seed-নির্ভর নয়, শুধু এটিকে খুঁজে বের করতে ঠিক কতবার সময় লাগবে তা seed-নির্ভর। গুরুত্বপূর্ণ বিষয় হলো, একই seed দিয়ে বারবার চালালে ফলাফল প্রতিবার হুবহু একই থাকে।

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

আগের পাঠ
মিউটেশন টেস্টিং